892873710e70213fc460a7fca87a191d663b0f2a
[python-delta-tar] / deltatar / crypto.py
1 #!/usr/bin/env python3
2
3 """
4 Intra2net 2017
5
6 ===============================================================================
7               crypto -- Encryption Layer for the Deltatar Backup
8 ===============================================================================
9
10 Crypto stack:
11
12     - AES-GCM for the symmetric encryption;
13     - Scrypt as KDF.
14
15 References:
16
17     - NIST Recommendation for Block Cipher Modes of Operation: Galois/Counter
18       Mode (GCM) and GMAC
19       http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
20
21     - AES-GCM v1:
22       https://cryptome.org/2014/01/aes-gcm-v1.pdf
23
24     - Authentication weaknesses in GCM
25       http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/comments/CWC-GCM/Ferguson2.pdf
26
27 Errors
28 -------------------------------------------------------------------------------
29
30 Errors fall into roughly three categories:
31
32     - Cryptographical errors or invalid data.
33
34         - ``InvalidGCMTag`` (decryption failed on account of an invalid GCM
35           tag),
36         - ``InvalidIVFixedPart`` (IV fixed part of object not found in list),
37         - ``DuplicateIV`` (the IV of an encrypted object already occurred),
38         - ``DecryptionError`` (used in CLI decryption for presenting error
39           conditions to the user).
40
41     - Incorrect usage of the library.
42
43         - ``InvalidParameter`` (non-conforming user supplied parameter),
44         - ``InvalidHeader`` (data passed for reading not parsable into header),
45         - ``FormatError`` (cannot handle header or parameter version),
46         - ``RuntimeError``.
47
48     - Bad internal state. If one of these is encountered it means that a state
49       was reached that shouldn’t occur during normal processing.
50
51         - ``InternalError``,
52         - ``Unreachable``.
53
54 Also, ``EndOfFile`` is used as a sentinel to communicate that a stream supplied
55 for reading is exhausted.
56
57 Initialization Vectors
58 -------------------------------------------------------------------------------
59
60 Initialization vectors are checked for reuse during the lifetime of a decryptor.
61 The fixed counters for metadata files cannot be reused and attempts to do so
62 will cause a DuplicateIV error. This means the length of objects encrypted with
63 a metadata counter is capped at 63 GB.
64
65 For ordinary, non-metadata payload, there is an optional mode with strict IV
66 checking that causes a crypto context to fail if an IV encountered or created
67 was already used for decrypting or encrypting, respectively, an earlier object.
68 Note that this mode can trigger false positives when decrypting non-linearly,
69 e. g. when traversing the same object multiple times. Since the crypto context
70 has no notion of a position in a PDT encrypted archive, this condition must be
71 sorted out downstream.
72
73 Command Line Utility
74 -------------------------------------------------------------------------------
75
76 ``crypto.py`` may be invoked as a script for decrypting, validating, and
77 splitting PDT encrypted files. Consult the usage message for details.
78
79 Usage examples:
80
81 Decrypt from stdin using the password ‘foo’: ::
82
83     $ crypto.py process foo -i - -o - <some-file.tar.gz.pdtcrypt >some-file.tar.gz
84
85 Output verbose information about the encrypted objects in the archive: ::
86
87     $ crypto.py process foo -v -i some-file.tar.gz.pdtcrypt -o /dev/null
88     PDT: decrypt from some-file.tar.gz.pdtcrypt
89     PDT: decrypt to /dev/null
90     PDT: source: file some-file.tar.gz.pdtcrypt
91     PDT: sink: file /dev/null
92     PDT: 0 hdr
93     PDT:    · version         = 1                    : 0100
94     PDT:    · paramversion    = 1                    : 0100
95     PDT:    · nacl                                   : d270 b031 00d1 87e2 c946 610d 7b7f 7e5f
96     PDT:    · iv                                     : 02ee 3dd7 a963 1eb1 0100 0000
97     PDT:    · ctsize          = 591                  : 4f02 0000 0000 0000
98     PDT:    · tag                                    : 5b2d 6d8b 8f82 4842 12fd 0b10 b6e3 369b
99     PDT: 64 decrypt obj no. 1,  591 B
100     PDT:    · [64] 0% done, read block (591 B of 591 B remaining)
101     PDT:    · decrypt ciphertext 591 B
102     PDT:    · decrypt plaintext 591 B
103     PDT: 655 finalize
104     …
105
106 Also, the mode *scrypt* allows deriving encryption keys. To calculate the
107 encryption key from the password ‘foo’ and the salt of the first object in a
108 PDT encrypted file: ::
109
110     $ crypto.py scrypt foo -i some-file.pdtcrypt
111     {"paramversion": 1, "salt": "Cqzbk48e3peEjzWto8D0yA==", "key": "JH9EkMwaM4x9F5aim5gK/Q=="}
112
113 The computed 16 byte key is given in hexadecimal notation in the value to
114 ``hash`` and can be fed into Python’s ``binascii.unhexlify()`` to obtain the
115 corresponding binary representation.
116
117 Note that in Scrypt hashing mode, no data integrity checks are being performed.
118 If the wrong password is given, a wrong key will be derived. Whether the password
119 was indeed correct can only be determined by decrypting. Note that since PDT
120 archives essentially consist of a stream of independent objects, the salt and
121 other parameters may change. Thus a key derived using above method from the
122 first object doesn’t necessarily apply to any of the subsequent objects.
123
124 """
125
126 import base64
127 import binascii
128 import bisect
129 import ctypes
130 import io
131 from functools import reduce, partial
132 import mmap
133 import os
134 import struct
135 import stat
136 import sys
137 import time
138 import types
139 import errno
140 try:
141     import enum34
142 except ImportError as exn:
143     pass
144
145 if __name__ == "__main__": ## Work around the import mechanism lest Python’s
146     pwd = os.getcwd()      ## preference for local imports causes a cyclical
147     ## import (crypto → pylibscrypt → […] → ./tarfile → crypto).
148     sys.path = [ p for p in sys.path if p.find ("deltatar") < 0 ]
149
150 import pylibscrypt
151 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
152 from cryptography.hazmat.backends import default_backend
153 import cryptography
154
155
156 __all__ = [ "hdr_make", "hdr_read", "hdr_fmt", "hdr_fmt_pretty"
157           , "scrypt_hashfile"
158           , "PDTCRYPT_HDR_SIZE", "AES_GCM_IV_CNT_DATA"
159           , "AES_GCM_IV_CNT_INFOFILE", "AES_GCM_IV_CNT_INDEX"
160           ]
161
162
163 ###############################################################################
164 ## exceptions
165 ###############################################################################
166
167 class EndOfFile (Exception):
168     """Reached EOF."""
169     remainder = 0
170     msg       = 0
171     def __init__ (self, n=None, msg=None):
172         if n is not None:
173             self.remainder = n
174         self.msg = msg
175
176
177 class InvalidParameter (Exception):
178     """Inputs not valid for PDT encryption."""
179     pass
180
181
182 class InvalidHeader (Exception):
183     """Header not valid."""
184     pass
185
186
187 class InvalidGCMTag (Exception):
188     """
189     The GCM tag calculated during decryption differs from that in the object
190     header.
191     """
192     pass
193
194
195 class InvalidIVFixedPart (Exception):
196     """
197     IV fixed part not in supplied list: either the backup is corrupt or the
198     current object does not belong to it.
199     """
200     pass
201
202
203 class IVFixedPartError (Exception):
204     """
205     Error creating a unique IV fixed part: repeated calls to system RNG yielded
206     the same sequence of bytes as the last IV used.
207     """
208     pass
209
210
211 class InvalidFileCounter (Exception):
212     """
213     When encrypting, an attempted reuse of a dedicated counter (info file,
214     index file) was caught.
215     """
216     pass
217
218
219 class DuplicateIV (Exception):
220     """
221     During encryption, the current IV fixed part is identical to an already
222     existing IV (same prefix and file counter). This indicates tampering or
223     programmer error and cannot be recovered from.
224     """
225     pass
226
227
228 class NonConsecutiveIV (Exception):
229     """
230     IVs not numbered consecutively. This is a hard error with strict IV
231     checking. Precludes random access to the encrypted objects.
232     """
233     pass
234
235
236 class FormatError (Exception):
237     """Unusable parameters in header."""
238     pass
239
240
241 class DecryptionError (Exception):
242     """Error during decryption with ``crypto.py`` on the command line."""
243     pass
244
245
246 class Unreachable (Exception):
247     """
248     Makeshift __builtin_unreachable(); always a programmer error if
249     thrown.
250     """
251     pass
252
253
254 class InternalError (Exception):
255     """Errors not ascribable to bad user inputs or cryptography."""
256     pass
257
258
259 ###############################################################################
260 ## crypto layer version
261 ###############################################################################
262
263 ENCRYPTION_PARAMETERS = \
264     { 0: \
265         { "kdf": ("dummy", 16)
266         , "enc": "passthrough" }
267     , 1: \
268         { "kdf": ( "scrypt"
269                  , { "dkLen"    : 16
270                    , "N"        : 1 << 16
271                    , "r"        : 8
272                    , "p"        : 1
273                    , "NaCl_LEN" : 16 })
274         , "enc": "aes-gcm" } }
275
276 ###############################################################################
277 ## constants
278 ###############################################################################
279
280 PDTCRYPT_HDR_MAGIC = b"PDTCRYPT"
281
282 PDTCRYPT_HDR_SIZE_MAGIC          = 8     #  8
283 PDTCRYPT_HDR_SIZE_VERSION        = 2     # 10
284 PDTCRYPT_HDR_SIZE_PARAMVERSION   = 2     # 12
285 PDTCRYPT_HDR_SIZE_NACL           = 16    # 28
286 PDTCRYPT_HDR_SIZE_IV             = 12    # 40
287 PDTCRYPT_HDR_SIZE_CTSIZE         = 8     # 48
288 PDTCRYPT_HDR_SIZE_TAG            = 16    # 64 GCM auth tag
289
290 PDTCRYPT_HDR_SIZE = PDTCRYPT_HDR_SIZE_MAGIC        + PDTCRYPT_HDR_SIZE_VERSION \
291                   + PDTCRYPT_HDR_SIZE_PARAMVERSION + PDTCRYPT_HDR_SIZE_NACL    \
292                   + PDTCRYPT_HDR_SIZE_IV           + PDTCRYPT_HDR_SIZE_CTSIZE  \
293                   + PDTCRYPT_HDR_SIZE_TAG # = 64
294
295 # precalculate offsets since Python can’t do constant folding over names
296 HDR_OFF_VERSION      = PDTCRYPT_HDR_SIZE_MAGIC
297 HDR_OFF_PARAMVERSION = HDR_OFF_VERSION      + PDTCRYPT_HDR_SIZE_VERSION
298 HDR_OFF_NACL         = HDR_OFF_PARAMVERSION + PDTCRYPT_HDR_SIZE_PARAMVERSION
299 HDR_OFF_IV           = HDR_OFF_NACL         + PDTCRYPT_HDR_SIZE_NACL
300 HDR_OFF_CTSIZE       = HDR_OFF_IV           + PDTCRYPT_HDR_SIZE_IV
301 HDR_OFF_TAG          = HDR_OFF_CTSIZE       + PDTCRYPT_HDR_SIZE_CTSIZE
302
303 FMT_UINT16_LE = "<H"
304 FMT_UINT64_LE = "<Q"
305 FMT_I2N_IV    = "<8sL"   # 8 random bytes ‖ 32 bit counter
306 FMT_I2N_HDR   = ("<"     # host byte order
307                  "8s"    # magic
308                  "H"     # version
309                  "H"     # paramversion
310                  "16s"   # sodium chloride
311                  "12s"   # iv
312                  "Q"     # size
313                  "16s")  # GCM tag
314
315 # aes+gcm
316 AES_KEY_SIZE                  = 16 # b"0123456789abcdef"
317 AES_KEY_SIZE_B64              = 24 # b'MDEyMzQ1Njc4OWFiY2RlZg=='
318
319 AES_GCM_MAX_SIZE              = (1 << 36) - (1 << 5) # 2^39 - 2^8 b ≅ 64 GB.
320                                 # Source: NIST SP 800-38D section 5.2.1.1
321 # https://crypto.stackexchange.com/questions/31793/plain-text-size-limits-for-aes-gcm-mode-just-64gb
322
323 PDTCRYPT_MAX_OBJ_SIZE_DEFAULT = 63 * (1 << 30)       #                63 GB
324 PDTCRYPT_MAX_OBJ_SIZE         = PDTCRYPT_MAX_OBJ_SIZE_DEFAULT
325
326 # index and info files are written on-the fly while encrypting so their
327 # counters must be available in advance
328 AES_GCM_IV_CNT_INFOFILE     = 1 # constant
329 AES_GCM_IV_CNT_INDEX        = AES_GCM_IV_CNT_INFOFILE + 1
330 AES_GCM_IV_CNT_DATA         = AES_GCM_IV_CNT_INDEX    + 1 # also for multivolume
331 AES_GCM_IV_CNT_MAX_DEFAULT  = 0xffFFffFF
332 AES_GCM_IV_CNT_MAX          = AES_GCM_IV_CNT_MAX_DEFAULT
333
334 # IV structure and generation
335 PDTCRYPT_IV_GEN_MAX_RETRIES = 10 # ×
336 PDTCRYPT_IV_FIXEDPART_SIZE  =  8 # B
337 PDTCRYPT_IV_COUNTER_SIZE    =  4 # B
338
339 # secret type: PW of string | KEY of char [16]
340 PDTCRYPT_SECRET_PW   = 0
341 PDTCRYPT_SECRET_KEY  = 1
342
343 ###############################################################################
344 ## header, trailer
345 ###############################################################################
346 #
347 # Interface:
348 #
349 #    struct hdrinfo
350 #      { version      : u16
351 #      , paramversion : u16
352 #      , nacl         : [u8; 16]
353 #      , iv           : [u8; 12]
354 #      , ctsize       : usize
355 #      , tag          : [u8; 16] }
356 #
357 #    fn hdr_read (f : handle) -> hdrinfo;
358 #    fn hdr_make (f : handle, h : hdrinfo) -> IOResult<usize>;
359 #    fn hdr_fmt (h : hdrinfo) -> String;
360 #
361
362 def hdr_read (data):
363     """
364     Read bytes as header structure.
365
366     If the input could not be interpreted as a header, fail with
367     ``InvalidHeader``.
368     """
369
370     try:
371         mag, version, paramversion, nacl, iv, ctsize, tag = \
372             struct.unpack (FMT_I2N_HDR, data)
373     except Exception as exn:
374         raise InvalidHeader ("error unpacking header from [%r]: %s"
375                              % (binascii.hexlify (data), str (exn)))
376
377     if mag != PDTCRYPT_HDR_MAGIC:
378         raise InvalidHeader ("bad magic in header: expected [%s], got [%s]"
379                              % (PDTCRYPT_HDR_MAGIC, mag))
380
381     return \
382         {      "version" : version
383         , "paramversion" : paramversion
384         ,         "nacl" : nacl
385         ,           "iv" : iv
386         ,       "ctsize" : ctsize
387         ,          "tag" : tag
388         }
389
390
391 def hdr_read_stream (instr):
392     """
393     Read header from stream at the current position.
394
395     Fail with ``InvalidHeader`` if insufficient bytes were read from the
396     stream, or if the content could not be interpreted as a header.
397     """
398     data = instr.read(PDTCRYPT_HDR_SIZE)
399     ldata = len (data)
400     if ldata == 0:
401         raise EndOfFile
402     elif ldata != PDTCRYPT_HDR_SIZE:
403         raise InvalidHeader ("hdr_read_stream: expected %d B, received %d B"
404                              % (PDTCRYPT_HDR_SIZE, ldata))
405     return hdr_read (data)
406
407
408 def hdr_from_params (version, paramversion, nacl, iv, ctsize, tag):
409     """
410     Assemble the necessary values into a PDTCRYPT header.
411
412     :type      version: int to fit   uint16_t
413     :type paramversion: int to fit   uint16_t
414     :type         nacl: bytes to fit uint8_t[16]
415     :type           iv: bytes to fit uint8_t[12]
416     :type         size: int to fit   uint64_t
417     :type          tag: bytes to fit uint8_t[16]
418     """
419     buf  = bytearray (PDTCRYPT_HDR_SIZE)
420     bufv = memoryview (buf)
421
422     try:
423         struct.pack_into (FMT_I2N_HDR, bufv, 0,
424                           PDTCRYPT_HDR_MAGIC,
425                           version, paramversion, nacl, iv, ctsize, tag)
426     except Exception as exn:
427         return False, "error assembling header: %s" % str (exn)
428
429     return True, bytes (buf)
430
431
432 def hdr_make_dummy (s):
433     """
434     Create a header sized block of bytes initialized to a value derived from a
435     string. Used to verify we’ve jumped back correctly to the actual position
436     of the object header.
437     """
438     c = reduce (lambda a, c: a + ord(c), s, 0) % 0xFF
439     return bytes (bytearray (struct.pack ("B", c)) * PDTCRYPT_HDR_SIZE)
440
441
442 def hdr_make (hdr):
443     """
444     Assemble a header from the given header structure.
445     """
446     return hdr_from_params (version=hdr.get("version"),
447                             paramversion=hdr.get("paramversion"),
448                             nacl=hdr.get("nacl"), iv=hdr.get("iv"),
449                             ctsize=hdr.get("ctsize"), tag=hdr.get("tag"))
450
451
452 HDR_FMT = "I2n_header { version: %d, paramversion: %d, nacl: %s[%d]," \
453                       " iv: %s[%d], ctsize: %d, tag: %s[%d] }"
454
455 def hdr_fmt (h):
456     """Format a header structure into readable output."""
457     return HDR_FMT % (h["version"], h["paramversion"],
458                       binascii.hexlify (h["nacl"]), len(h["nacl"]),
459                       binascii.hexlify (h["iv"]), len(h["iv"]),
460                       h["ctsize"],
461                       binascii.hexlify (h["tag"]), len(h["tag"]))
462
463
464 def hex_spaced_of_bytes (b):
465     """Format bytes object, hexdump style."""
466     return " ".join ([ "%.2x%.2x" % (c1, c2)
467                        for c1, c2 in zip (b[0::2], b[1::2]) ]) \
468          + (len (b) | 1 == len (b) and " %.2x" % b[-1] or "") # odd lengths
469
470
471 def hdr_iv_counter (h):
472     """Extract the variable part of the IV of the given header."""
473     _fixed, cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
474     return cnt
475
476
477 def hdr_iv_fixed (h):
478     """Extract the fixed part of the IV of the given header."""
479     fixed, _cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
480     return fixed
481
482
483 hdr_dump = hex_spaced_of_bytes
484
485
486 HDR_FMT_PRETTY = \
487 """version         = %-4d                 : %s
488 paramversion    = %-4d                 : %s
489 nacl                                   : %s
490 iv                                     : %s
491 ctsize          = %-20d : %s
492 tag                                    : %s
493 """
494
495 def hdr_fmt_pretty (h):
496     """
497     Format header structure into multi-line representation of its contents and
498     their raw representation. (Omit the implicit “PDTCRYPT” magic bytes that
499     precede every header.)
500     """
501     return HDR_FMT_PRETTY \
502                 % (h["version"],
503                    hex_spaced_of_bytes (struct.pack (FMT_UINT16_LE, h["version"])),
504                    h["paramversion"],
505                    hex_spaced_of_bytes (struct.pack (FMT_UINT16_LE, h["paramversion"])),
506                    hex_spaced_of_bytes (h["nacl"]),
507                    hex_spaced_of_bytes (h["iv"]),
508                    h["ctsize"],
509                    hex_spaced_of_bytes (struct.pack (FMT_UINT64_LE, h["ctsize"])),
510                    hex_spaced_of_bytes (h["tag"]))
511
512 IV_FMT = "((f %s) (c %d))"
513
514 def iv_fmt (iv):
515     """Format the two components of an IV in a readable fashion."""
516     fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
517     return IV_FMT % (binascii.hexlify (fixed), cnt)
518
519
520 ###############################################################################
521 ## restoration
522 ###############################################################################
523
524 class Location (object):
525     n = 0
526     offset = 0
527
528 def restore_loc_fmt (loc):
529     return "%d off:%d" \
530         % (loc.n, loc.offset)
531
532 def locate_hdr_candidates (fd):
533     """
534     Walk over instances of the magic string in the payload, collecting their
535     positions. If the offset of the first found instance is not zero, the file
536     begins with leading garbage. Used by desaster recovery.
537
538     :return:    The list of offsets in the file.
539     """
540     cands = []
541
542     mm = mmap.mmap(fd, 0, mmap.MAP_SHARED, mmap.PROT_READ)
543     pos = 0
544     while True:
545         pos = mm.find (PDTCRYPT_HDR_MAGIC, pos)
546         if pos == -1:
547             break
548         cands.append (pos)
549         pos += 1
550
551     return cands
552
553
554 HDR_CAND_GOOD       = 0 # header marks begin of valid object
555 HDR_CAND_FISHY      = 1 # inconclusive (tag mismatch, obj overlap etc.)
556 HDR_CAND_JUNK       = 2 # not a header / object unreadable
557
558 HDR_VERDICT_NAME = \
559     { HDR_CAND_GOOD  : "valid"
560     , HDR_CAND_FISHY : "fishy"
561     , HDR_CAND_JUNK  : "junk"
562     }
563
564
565 def verdict_fmt (vdt):
566     return HDR_VERDICT_NAME [vdt]
567
568
569 def inspect_hdr (fd, off):
570     """
571     Attempt to parse a header in *fd* at position *off*.
572
573     Returns a verdict about the quality of that header plus the parsed header
574     when readable.
575     """
576
577     _ = os.lseek (fd, off, os.SEEK_SET)
578
579     if os.lseek (fd, 0, os.SEEK_CUR) != off:
580         if PDTCRYPT_VERBOSE is True:
581             noise ("PDT: %d → dismissed (lseek() past EOF)" % off)
582         return HDR_CAND_JUNK, None
583
584     raw = os.read (fd, PDTCRYPT_HDR_SIZE)
585     if len (raw) != PDTCRYPT_HDR_SIZE:
586         if PDTCRYPT_VERBOSE is True:
587             noise ("PDT: %d → dismissed (EOF inside header)" % off)
588         return HDR_CAND_JUNK, None
589
590     try:
591         hdr = hdr_read (raw)
592     except InvalidHeader as exn:
593         if PDTCRYPT_VERBOSE is True:
594             noise ("PDT: %d → dismissed (invalid: [%s])" % (off, str (exn)))
595         return HDR_CAND_JUNK, None
596
597     obj0 = off + PDTCRYPT_HDR_SIZE
598     objX = obj0 + hdr ["ctsize"]
599
600     eof  = os.lseek (fd, 0, os.SEEK_END)
601     if eof < objX:
602         if PDTCRYPT_VERBOSE is True:
603             noise ("PDT: %d → EOF inside object (%d≤%d≤%d); adjusting size to "
604                    "%d" % (off, obj0, eof, objX, (eof - obj0)))
605         # try reading up to the end
606         hdr ["ctsize"] = eof - obj0
607         return HDR_CAND_FISHY, hdr
608
609     return HDR_CAND_GOOD, hdr
610
611
612 def try_decrypt (ifd, off, hdr, secret, ofd=-1):
613     """
614     Attempt to decrypt the object in the (seekable) descriptor *ifd* starting
615     at *off* using the metadata in *hdr* and *secret*. An output fd can be
616     specified with *ofd*; if it is *-1* – the default –, the decrypted payload
617     will be discarded.
618
619     Always creates a fresh decryptor, so validation steps across objects don’t
620     apply.
621
622     Errors during GCM tag validation are ignored. Used by desaster recovery.
623     """
624     ctleft = hdr ["ctsize"]
625     pos    = off
626
627     ks = secret [0]
628     if ks == PDTCRYPT_SECRET_PW:
629         decr = Decrypt (password=secret [1])
630     elif ks == PDTCRYPT_SECRET_KEY:
631         key = secret [1]
632         decr = Decrypt (key=key)
633     else:
634         raise RuntimeError
635
636     decr.next (hdr)
637
638     try:
639         os.lseek (ifd, pos, os.SEEK_SET)
640         pt = b""
641         while ctleft > 0:
642             cnksiz = min (ctleft, PDTCRYPT_BLOCKSIZE)
643             cnk    = os.read (ifd, cnksiz)
644             ctleft -= cnksiz
645             pos    += cnksiz
646             pt     = decr.process (cnk)
647             if ofd != -1:
648                 os.write (ofd, pt)
649         try:
650             pt = decr.done ()
651         except InvalidGCMTag:
652             noise ("PDT: GCM tag mismatch for object %d–%d"
653                    % (off, off + hdr ["ctsize"]))
654         if len (pt) > 0 and ofd != -1:
655             os.write (ofd, pt)
656
657     except Exception as exn:
658         noise ("PDT: error decrypting object %d–%d@%d, %d B remaining [%s]"
659                % (off, off + hdr ["ctsize"], pos, ctleft, exn))
660         raise
661
662     return pos - off
663
664
665 def readable_objects_offsets (ifd, secret, cands):
666     """
667     From a list of candidates, locate the ones that mark the start of actual
668     readable PDTCRYPT objects.
669     """
670     good = []
671
672     for i, cand in enumerate (cands):
673         vdt, hdr = inspect_hdr (ifd, cand)
674         if vdt == HDR_CAND_JUNK:
675             pass # ignore unreadable ones
676         elif vdt in [HDR_CAND_GOOD, HDR_CAND_FISHY]:
677             ctsize =  hdr ["ctsize"]
678             off0 = cand + PDTCRYPT_HDR_SIZE
679             ok = try_decrypt (ifd, off0, hdr, secret) == ctsize
680             if ok is True:
681                 good.append ((cand, off0 + ctsize))
682
683     overlap = find_overlaps (good)
684
685     return [ g [0] for g in good ]
686
687
688 def reconstruct_offsets (fname, secret):
689     ifd = os.open (fname, os.O_RDONLY)
690
691     try:
692         cands = locate_hdr_candidates (ifd)
693         return readable_objects_offsets (ifd, secret, cands)
694     finally:
695         os.close (ifd)
696
697
698 ###############################################################################
699 ## helpers
700 ###############################################################################
701
702 def make_secret (password=None, key=None):
703     """
704     Safely create a “secret” value that consists either of a key or a password.
705     Inputs are validated: the password is accepted as (UTF-8 encoded) bytes or
706     string; for the key only a bytes object of the proper size or a base64
707     encoded string thereof is accepted.
708
709     If both are provided, the key is preferred over the password; no checks are
710     performed whether the key is derived from the password.
711
712     :returns: secret value if inputs were acceptable | None otherwise.
713     """
714     if key is not None:
715         if isinstance (key, str) is True:
716             key = key.encode ("utf-8")
717         if isinstance (key, bytes) is True:
718             if len (key) == AES_KEY_SIZE:
719                 return (PDTCRYPT_SECRET_KEY, key)
720             if len (key) == AES_KEY_SIZE * 2:
721                 try:
722                     key = binascii.unhexlify (key)
723                     return (PDTCRYPT_SECRET_KEY, key)
724                 except binascii.Error: # garbage in string
725                     pass
726             if len (key) == AES_KEY_SIZE_B64:
727                 try:
728                     key = base64.b64decode (key)
729                     # the base64 processor is very tolerant and allows for
730                     # arbitrary trailing and leading data thus the data obtained
731                     # must be checked for the proper length
732                     if len (key) == AES_KEY_SIZE:
733                         return (PDTCRYPT_SECRET_KEY, key)
734                 except binascii.Error: # “incorrect padding”
735                     pass
736     elif password is not None:
737         if isinstance (password, str) is True:
738             return (PDTCRYPT_SECRET_PW, password)
739         elif isinstance (password, bytes) is True:
740             try:
741                 password = password.decode ("utf-8")
742                 return (PDTCRYPT_SECRET_PW, password)
743             except UnicodeDecodeError:
744                 pass
745
746     return None
747
748
749 ###############################################################################
750 ## passthrough / null encryption
751 ###############################################################################
752
753 class PassthroughCipher (object):
754
755     tag = struct.pack ("<QQ", 0, 0)
756
757     def __init__          (self)    : pass
758
759     def update            (self, b) : return b
760
761     def finalize          (self)    : return b""
762
763     def finalize_with_tag (self, _) : return b""
764
765 ###############################################################################
766 ## convenience wrapper
767 ###############################################################################
768
769
770 def kdf_dummy (klen, password, _nacl):
771     """
772     Fake KDF for testing purposes that is called when parameter version zero is
773     encountered.
774     """
775     q, r = divmod (klen, len (password))
776     if isinstance (password, bytes) is False:
777         password = password.encode ()
778     return password * q + password [:r], b""
779
780
781 SCRYPT_KEY_MEMO = { } # static because needed for both the info file and the archive
782
783
784 def kdf_scrypt (params, password, nacl):
785     """
786     Wrapper for the Scrypt KDF, corresponds to parameter version one. The
787     computation result is memoized based on the inputs to facilitate spawning
788     multiple encryption contexts.
789     """
790     N = params["N"]
791     r = params["r"]
792     p = params["p"]
793     dkLen = params["dkLen"]
794
795     if nacl is None:
796         nacl = os.urandom (params["NaCl_LEN"])
797
798     key_parms = (password, nacl, N, r, p, dkLen)
799     global SCRYPT_KEY_MEMO
800     if key_parms not in SCRYPT_KEY_MEMO:
801         SCRYPT_KEY_MEMO [key_parms] = \
802             pylibscrypt.scrypt (password, nacl, N, r, p, dkLen)
803     return SCRYPT_KEY_MEMO [key_parms], nacl
804
805
806 def kdf_by_version (paramversion=None, defs=None):
807     """
808     Pick the KDF handler corresponding to the parameter version or the
809     definition set.
810
811     :rtype: function (password : str, nacl : str) -> str
812     """
813     if paramversion is not None:
814         defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
815     if defs is None:
816         raise InvalidParameter ("no encryption parameters for version %r"
817                                 % paramversion)
818     (kdf, params) = defs["kdf"]
819     fn = None
820     if kdf == "scrypt" : fn = kdf_scrypt
821     if kdf == "dummy"  : fn = kdf_dummy
822     if fn is None:
823         raise ValueError ("key derivation method %r unknown" % kdf)
824     return partial (fn, params)
825
826
827 ###############################################################################
828 ## SCRYPT hashing
829 ###############################################################################
830
831 def scrypt_hashsource (pw, ins):
832     """
833     Calculate the SCRYPT hash from the password and the information contained
834     in the first header found in ``ins``.
835
836     This does not validate whether the first object is encrypted correctly.
837     """
838     if isinstance (pw, str) is True:
839         pw = str.encode (pw)
840     elif isinstance (pw, bytes) is False:
841         raise InvalidParameter ("password must be a string, not %s"
842                                 % type (pw))
843     if isinstance (ins, io.BufferedReader) is False and \
844             isinstance (ins, io.FileIO) is False:
845         raise InvalidParameter ("file to hash must be opened in “binary” mode")
846     hdr = None
847     try:
848         hdr = hdr_read_stream (ins)
849     except EndOfFile as exn:
850         noise ("PDT: malformed input: end of file reading first object header")
851         noise ("PDT:")
852         return 1
853
854     nacl = hdr ["nacl"]
855     pver = hdr ["paramversion"]
856     if PDTCRYPT_VERBOSE is True:
857         noise ("PDT: salt of first object          : %s" % binascii.hexlify (nacl))
858         noise ("PDT: parameter version of archive  : %d" % pver)
859
860     try:
861         defs = ENCRYPTION_PARAMETERS.get(pver, None)
862         kdfname, params = defs ["kdf"]
863         if kdfname != "scrypt":
864             noise ("PDT: input is not an SCRYPT archive")
865             noise ("")
866             return 1
867         kdf = kdf_by_version (None, defs)
868     except ValueError as exn:
869         noise ("PDT: object has unknown parameter version %d" % pver)
870
871     hsh, _void = kdf (pw, nacl)
872
873     return hsh, nacl, hdr ["version"], pver
874
875
876 def scrypt_hashfile (pw, fname):
877     """
878     Calculate the SCRYPT hash from the password and the information contained
879     in the first header found in the given file. The header is read only at
880     offset zero.
881     """
882     with deptdcrypt_mk_stream (PDTCRYPT_SOURCE, fname  or "-") as ins:
883         hsh, _void, _void, _void = scrypt_hashsource (pw, ins)
884         return hsh
885
886
887 ###############################################################################
888 ## AES-GCM context
889 ###############################################################################
890
891 class Crypto (object):
892     """
893     Encryption context to remain alive throughout an entire tarfile pass.
894     """
895     enc  = None
896     nacl = None
897     key  = None
898     cnt  = None # file counter (uint32_t != 0)
899     iv   = None # current IV
900     fixed        = None  # accu for 64 bit fixed parts of IV
901     used_ivs     = None  # tracks IVs
902     strict_ivs   = False # if True, panic on duplicate object IV
903     password     = None
904     paramversion = None
905     stats = { "in"  : 0
906             , "out" : 0
907             , "obj" : 0 }
908
909     ctsize  = -1
910     ptsize  = -1
911     info_counter_used  = False
912     index_counter_used = False
913
914     def __init__ (self, *al, **akv):
915         self.used_ivs = set ()
916         self.set_parameters (*al, **akv)
917
918
919     def next_fixed (self):
920         # NOP for decryption
921         pass
922
923
924     def set_object_counter (self, cnt=None):
925         """
926         Safely set the internal counter of encrypted objects. Numerous
927         constraints apply:
928
929         The same counter may not be reused in combination with one IV fixed
930         part. This is validated elsewhere in the IV handling.
931
932         Counter zero is invalid. The first two counters are reserved for
933         metadata. The implementation does not allow for splitting metadata
934         files over multiple encrypted objects. (This would be possible by
935         assigning new fixed parts.) Thus in a Deltatar backup there is at most
936         one object with a counter value of one and two. On creation of a
937         context, the initial counter may be chosen. The globals
938         ``AES_GCM_IV_CNT_INFOFILE`` and ``AES_GCM_IV_CNT_INDEX`` can be used to
939         request one of the reserved values. If one of these values has been
940         used, any further attempt of setting the counter to that value will
941         be rejected with an ``InvalidFileCounter`` exception.
942
943         Out of bounds values (i. e. below one and more than the maximum of 2³²)
944         cause an ``InvalidParameter`` exception to be thrown.
945         """
946         if cnt is None:
947             self.cnt = AES_GCM_IV_CNT_DATA
948             return
949         if cnt == 0 or cnt > AES_GCM_IV_CNT_MAX + 1:
950             raise InvalidParameter ("invalid counter value %d requested: "
951                                     "acceptable values are from 1 to %d"
952                                     % (cnt, AES_GCM_IV_CNT_MAX))
953         if cnt == AES_GCM_IV_CNT_INFOFILE:
954             if self.info_counter_used is True:
955                 raise InvalidFileCounter ("attempted to reuse info file "
956                                           "counter %d: must be unique" % cnt)
957             self.info_counter_used = True
958         elif cnt == AES_GCM_IV_CNT_INDEX:
959             if self.index_counter_used is True:
960                 raise InvalidFileCounter ("attempted to reuse index file "
961                                           " counter %d: must be unique" % cnt)
962             self.index_counter_used = True
963         if cnt <= AES_GCM_IV_CNT_MAX:
964             self.cnt = cnt
965             return
966         # cnt == AES_GCM_IV_CNT_MAX + 1 → wrap
967         self.cnt = AES_GCM_IV_CNT_DATA
968         self.next_fixed ()
969
970
971     def set_parameters (self, password=None, key=None, paramversion=None,
972                         nacl=None, counter=None, strict_ivs=False):
973         """
974         Configure the internal state of a crypto context. Not intended for
975         external use.
976         """
977         self.next_fixed ()
978         self.set_object_counter (counter)
979         self.strict_ivs = strict_ivs
980
981         if paramversion is not None:
982             self.paramversion = paramversion
983
984         if key is not None:
985             self.key, self.nacl = key, nacl
986             return
987
988         if password is not None:
989             if isinstance (password, bytes) is False:
990                 password = str.encode (password)
991             self.password = password
992             if paramversion is None and nacl is None:
993                 # postpone key setup until first header is available
994                 return
995             kdf = kdf_by_version (paramversion)
996             if kdf is not None:
997                 self.key, self.nacl = kdf (password, nacl)
998
999
1000     def process (self, buf):
1001         """
1002         Encrypt / decrypt a buffer. Invokes the ``.update()`` method on the
1003         wrapped encryptor or decryptor, respectively.
1004
1005         The Cryptography exception ``AlreadyFinalized`` is translated to an
1006         ``InternalError`` at this point. It may occur in sound code when the GC
1007         closes an encrypting stream after an error. Everywhere else it must be
1008         treated as a bug.
1009         """
1010         if self.enc is None:
1011             raise RuntimeError ("process: context not initialized")
1012         self.stats ["in"] += len (buf)
1013         try:
1014             out = self.enc.update (buf)
1015         except cryptography.exceptions.AlreadyFinalized as exn:
1016             raise InternalError (exn)
1017         self.stats ["out"] += len (out)
1018         return out
1019
1020
1021     def next (self, password, paramversion, nacl, iv):
1022         """
1023         Prepare for encrypting another object: Reset the data counters and
1024         change the configuration in case one of the variable parameters differs
1025         from the last object. Also check the IV for duplicates and error out
1026         if strict checking was requested.
1027         """
1028         self.ctsize = 0
1029         self.ptsize = 0
1030         self.stats ["obj"] += 1
1031
1032         self.check_duplicate_iv (iv)
1033
1034         if (   self.paramversion != paramversion
1035             or self.password     != password
1036             or self.nacl         != nacl):
1037             self.set_parameters (password=password, paramversion=paramversion,
1038                                  nacl=nacl, strict_ivs=self.strict_ivs)
1039
1040
1041     def check_duplicate_iv (self, iv):
1042         """
1043         Add an IV (the 12 byte representation as in the header) to the list. With
1044         strict checking enabled, this will throw a ``DuplicateIV``. Depending on
1045         the context, this may indicate a serious error (IV reuse).
1046         """
1047         if self.strict_ivs is True and iv in self.used_ivs:
1048             raise DuplicateIV ("iv %s was reused" % iv_fmt (iv))
1049         # vi has not been used before; add to collection
1050         self.used_ivs.add (iv)
1051
1052
1053     def counters (self):
1054         """
1055         Access the data counters.
1056         """
1057         return self.stats ["obj"], self.stats ["in"], self.stats ["out"]
1058
1059
1060     def drop (self):
1061         """
1062         Clear the current context regardless of its finalization state. The
1063         next operation must be ``.next()``.
1064         """
1065         self.enc = None
1066
1067
1068 class Encrypt (Crypto):
1069
1070     lastinfo     = None
1071     version      = None
1072     paramenc     = None
1073
1074     def __init__ (self, version, paramversion, password=None, key=None, nacl=None,
1075                   counter=AES_GCM_IV_CNT_DATA, strict_ivs=True):
1076         """
1077         The ctor will throw immediately if one of the parameters does not conform
1078         to our expectations.
1079
1080                   counter=AES_GCM_IV_CNT_DATA, strict_ivs=True):
1081         :type      version: int to fit   uint16_t
1082         :type paramversion: int to fit   uint16_t
1083         :param    password: mutually exclusive with ``key``
1084         :type     password: bytes
1085         :param         key: mutually exclusive with ``password``
1086         :type          key: bytes
1087         :type         nacl: bytes
1088         :type      counter: initial object counter the values
1089                             ``AES_GCM_IV_CNT_INFOFILE`` and
1090                             ``AES_GCM_IV_CNT_INDEX`` are unique in each backup set
1091                             and cannot be reused even with different fixed parts.
1092         :type   strict_ivs: bool
1093         """
1094         if         password is     None and key is     None \
1095                 or password is not None and key is not None :
1096             raise InvalidParameter ("__init__: need either key or password")
1097
1098         if key is not None:
1099             if isinstance (key, bytes) is False:
1100                 raise InvalidParameter ("__init__: key must be provided as "
1101                                         "bytes, not %s" % type (key))
1102             if nacl is None:
1103                 raise InvalidParameter ("__init__: salt must be provided along "
1104                                         "with encryption key")
1105         else: # password, no key
1106             if isinstance (password, str) is False:
1107                 raise InvalidParameter ("__init__: password must be a string, not %s"
1108                                         % type (password))
1109             if len (password) == 0:
1110                 raise InvalidParameter ("__init__: supplied empty password but not "
1111                                         "permitted for PDT encrypted files")
1112         # version
1113         if isinstance (version, int) is False:
1114             raise InvalidParameter ("__init__: version number must be an "
1115                                     "integer, not %s" % type (version))
1116         if version < 0:
1117             raise InvalidParameter ("__init__: version number must be a "
1118                                     "nonnegative integer, not %d" % version)
1119         # paramversion
1120         if isinstance (paramversion, int) is False:
1121             raise InvalidParameter ("__init__: crypto parameter version number "
1122                                     "must be an integer, not %s"
1123                                     % type (paramversion))
1124         if paramversion < 0:
1125             raise InvalidParameter ("__init__: crypto parameter version number "
1126                                     "must be a nonnegative integer, not %d"
1127                                     % paramversion)
1128         # salt
1129         if nacl is not None:
1130             if isinstance (nacl, bytes) is False:
1131                 raise InvalidParameter ("__init__: salt given, but of type %s "
1132                                         "instead of bytes" % type (nacl))
1133             # salt length would depend on the actual encryption so it can’t be
1134             # validated at this point
1135         self.fixed        = [ ]
1136         self.version      = version
1137         self.paramenc     = ENCRYPTION_PARAMETERS.get (paramversion) ["enc"]
1138
1139         super().__init__ (password, key, paramversion, nacl, counter=counter,
1140                           strict_ivs=strict_ivs)
1141
1142
1143     def next_fixed (self, retries=PDTCRYPT_IV_GEN_MAX_RETRIES):
1144         """
1145         Generate the next IV fixed part by reading eight bytes from
1146         ``/dev/urandom``. The buffer so obtained is tested against the fixed
1147         parts used so far to prevent accidental reuse of IVs. After a
1148         configurable number of attempts to create a unique fixed part, it will
1149         refuse to continue with an ``IVFixedPartError``. This is unlikely to
1150         ever happen on a normal system but may detect an issue with the random
1151         generator.
1152
1153         The list of fixed parts that were used by the context at hand can be
1154         accessed through the ``.fixed`` list. Its last element is the fixed
1155         part currently in use.
1156         """
1157         i = 0
1158         while i < retries:
1159             fp = os.urandom (PDTCRYPT_IV_FIXEDPART_SIZE)
1160             if fp not in self.fixed:
1161                 self.fixed.append (fp)
1162                 return
1163             i += 1
1164         raise IVFixedPartError ("error obtaining a unique IV fixed part from "
1165                                 "/dev/urandom; giving up after %d tries" % i)
1166
1167
1168     def iv_make (self):
1169         """
1170         Construct a 12-bytes IV from the current fixed part and the object
1171         counter.
1172         """
1173         return struct.pack(FMT_I2N_IV, self.fixed [-1], self.cnt)
1174
1175
1176     def next (self, filename=None, counter=None):
1177         """
1178         Prepare for encrypting the next incoming object. Update the counter
1179         and put together the IV, possibly changing prefixes. Then create the
1180         new encryptor.
1181
1182         The argument ``counter`` can be used to specify a file counter for this
1183         object. Unless it is one of the reserved values, the counter of
1184         subsequent objects will be computed from this one.
1185
1186         If this is the first object in a series, ``filename`` is required,
1187         otherwise it is reused if not present. The value is used to derive a
1188         header sized placeholder to use until after encryption when all the
1189         inputs to construct the final header are available. This is then
1190         matched in ``.done()`` against the value found at the position of the
1191         header. The motivation for this extra check is primarily to assist
1192         format debugging: It makes stray headers easy to spot in malformed
1193         PDTCRYPT files.
1194         """
1195         if filename is None:
1196             if self.lastinfo is None:
1197                 raise InvalidParameter ("next: filename is mandatory for "
1198                                         "first object")
1199             filename, _dummy = self.lastinfo
1200         else:
1201             if isinstance (filename, str) is False:
1202                 raise InvalidParameter ("next: filename must be a string, no %s"
1203                                         % type (filename))
1204         if counter is not None:
1205             if isinstance (counter, int) is False:
1206                 raise InvalidParameter ("next: the supplied counter is of "
1207                                         "invalid type %s; please pass an "
1208                                         "integer instead" % type (counter))
1209             self.set_object_counter (counter)
1210
1211         self.iv = self.iv_make ()
1212         if self.paramenc == "aes-gcm":
1213             self.enc = Cipher \
1214                             ( algorithms.AES (self.key)
1215                             , modes.GCM (self.iv)
1216                             , backend = default_backend ()) \
1217                             .encryptor ()
1218         elif self.paramenc == "passthrough":
1219             self.enc = PassthroughCipher ()
1220         else:
1221             raise InvalidParameter ("next: parameter version %d not known"
1222                                     % self.paramversion)
1223         hdrdum = hdr_make_dummy (filename)
1224         self.lastinfo = (filename, hdrdum)
1225         super().next (self.password, self.paramversion, self.nacl, self.iv)
1226
1227         self.set_object_counter (self.cnt + 1)
1228         return hdrdum
1229
1230
1231     def done (self, cmpdata):
1232         """
1233         Complete encryption of an object. After this has been called, attempts
1234         of encrypting further data will cause an error until ``.next()`` is
1235         invoked properly.
1236
1237         Returns a 64 bytes buffer containing the object header including all
1238         values including the “late” ones e. g. the ciphertext size and the
1239         GCM tag.
1240         """
1241         if isinstance (cmpdata, bytes) is False:
1242             raise InvalidParameter ("done: comparison input expected as bytes, "
1243                                     "not %s" % type (cmpdata))
1244         if self.lastinfo is None:
1245             raise RuntimeError ("done: encryption context not initialized")
1246         filename, hdrdum = self.lastinfo
1247         if cmpdata != hdrdum:
1248             raise RuntimeError ("done: bad sync of header for object %d: "
1249                                 "preliminary data does not match; this likely "
1250                                 "indicates a wrongly repositioned stream"
1251                                 % self.cnt)
1252         data = self.enc.finalize ()
1253         self.stats ["out"] += len (data)
1254         self.ctsize += len (data)
1255         ok, hdr = hdr_from_params (self.version, self.paramversion, self.nacl,
1256                                    self.iv, self.ctsize, self.enc.tag)
1257         if ok is False:
1258             raise InternalError ("error constructing header: %r" % hdr)
1259         return data, hdr, self.fixed
1260
1261
1262     def process (self, buf):
1263         """
1264         Encrypt a chunk of plaintext with the active encryptor. Returns the
1265         size of the input consumed. This **must** be checked downstream. If the
1266         maximum possible object size has been reached, the current context must
1267         be finalized and a new one established before any further data can be
1268         encrypted. The second argument is the remainder of the plaintext that
1269         was not encrypted for the caller to use immediately after the new
1270         context is ready.
1271         """
1272         if isinstance (buf, bytes) is False:
1273             raise InvalidParameter ("process: expected byte buffer, not %s"
1274                                     % type (buf))
1275         bsize = len (buf)
1276         newptsize = self.ptsize + bsize
1277         diff = newptsize - PDTCRYPT_MAX_OBJ_SIZE
1278         if diff > 0:
1279             bsize -= diff
1280             newptsize = PDTCRYPT_MAX_OBJ_SIZE
1281         self.ptsize = newptsize
1282         data = super().process (buf [:bsize])
1283         self.ctsize += len (data)
1284         return bsize, data
1285
1286
1287 class Decrypt (Crypto):
1288
1289     tag        = None   # GCM tag, part of header
1290     last_iv    = None   # check consecutive ivs in strict mode
1291
1292     def __init__ (self, password=None, key=None, counter=None, fixedparts=None,
1293                   strict_ivs=False):
1294         """
1295         Sanitizing ctor for the decryption context. ``fixedparts`` specifies a
1296         list of IV fixed parts accepted during decryption. If a fixed part is
1297         encountered that is not in the list, decryption will fail.
1298
1299         :param    password: mutually exclusive with ``key``
1300         :type     password: bytes
1301         :param         key: mutually exclusive with ``password``
1302         :type          key: bytes
1303         :type      counter: initial object counter the values
1304                             ``AES_GCM_IV_CNT_INFOFILE`` and
1305                             ``AES_GCM_IV_CNT_INDEX`` are unique in each backup set
1306                             and cannot be reused even with different fixed parts.
1307         :type   fixedparts: bytes list
1308         """
1309         if         password is     None and key is     None \
1310                 or password is not None and key is not None :
1311             raise InvalidParameter ("__init__: need either key or password")
1312
1313         if key is not None:
1314             if isinstance (key, bytes) is False:
1315                 raise InvalidParameter ("__init__: key must be provided as "
1316                                         "bytes, not %s" % type (key))
1317         else: # password, no key
1318             if isinstance (password, str) is False:
1319                 raise InvalidParameter ("__init__: password must be a string, not %s"
1320                                         % type (password))
1321             if len (password) == 0:
1322                 raise InvalidParameter ("__init__: supplied empty password but not "
1323                                         "permitted for PDT encrypted files")
1324         # fixed parts
1325         if fixedparts is not None:
1326             if isinstance (fixedparts, list) is False:
1327                 raise InvalidParameter ("__init__: IV fixed parts must be "
1328                                         "supplied as list, not %s"
1329                                         % type (fixedparts))
1330             self.fixed = fixedparts
1331             self.fixed.sort ()
1332
1333         super().__init__ (password=password, key=key, counter=counter,
1334                           strict_ivs=strict_ivs)
1335
1336
1337     def valid_fixed_part (self, iv):
1338         """
1339         Check if a fixed part was already seen.
1340         """
1341         # check if fixed part is known
1342         fixed, _cnt = struct.unpack (FMT_I2N_IV, iv)
1343         i = bisect.bisect_left (self.fixed, fixed)
1344         return i != len (self.fixed) and self.fixed [i] == fixed
1345
1346
1347     def check_consecutive_iv (self, iv):
1348         """
1349         Check whether the counter part of the given IV is indeed the successor
1350         of the currently present counter. This should always be the case for
1351         the objects in a well formed PDT archive but should not be enforced
1352         when decrypting out-of-order.
1353         """
1354         fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
1355         if self.strict_ivs is True \
1356                 and self.last_iv is not None \
1357                 and self.last_iv [0] == fixed \
1358                 and self.last_iv [1] != cnt - 1:
1359             raise NonConsecutiveIV ("iv %s counter not successor of "
1360                                     "last object (expected %d, found %d)"
1361                                     % (fixed, iv_fmt (self.last_iv [1]), cnt))
1362         self.last_iv = (fixed, cnt)
1363
1364
1365     def next (self, hdr):
1366         """
1367         Start decrypting the next object. The PDTCRYPT header for the object
1368         can be given either as already parsed object or as bytes.
1369         """
1370         if isinstance (hdr, bytes) is True:
1371             hdr = hdr_read (hdr)
1372         elif isinstance (hdr, dict) is False:
1373             # this won’t catch malformed specs though
1374             raise InvalidParameter ("next: wrong type of parameter hdr: "
1375                                     "expected bytes or spec, got %s"
1376                                     % type (hdr))
1377         try:
1378             paramversion = hdr ["paramversion"]
1379             nacl         = hdr ["nacl"]
1380             iv           = hdr ["iv"]
1381             tag          = hdr ["tag"]
1382         except KeyError:
1383             raise InvalidHeader ("next: not a header %r" % hdr)
1384
1385         super().next (self.password, paramversion, nacl, iv)
1386         if self.fixed is not None and self.valid_fixed_part (iv) is False:
1387             raise InvalidIVFixedPart ("iv %s has invalid fixed part"
1388                                       % iv_fmt (iv))
1389         self.check_consecutive_iv (iv)
1390
1391         self.tag = tag
1392         defs = ENCRYPTION_PARAMETERS.get (paramversion, None)
1393         if defs is None:
1394             raise FormatError ("header contains unknown parameter version %d; "
1395                                "maybe the file was created by a more recent "
1396                                "version of Deltatar" % paramversion)
1397         enc = defs ["enc"]
1398         if enc == "aes-gcm":
1399             self.enc = Cipher \
1400                             ( algorithms.AES (self.key)
1401                             , modes.GCM (iv, tag=self.tag)
1402                             , backend = default_backend ()) \
1403                             . decryptor ()
1404         elif enc == "passthrough":
1405             self.enc = PassthroughCipher ()
1406         else:
1407             raise InternalError ("encryption parameter set %d refers to unknown "
1408                                  "mode %r" % (paramversion, enc))
1409         self.set_object_counter (self.cnt + 1)
1410
1411
1412     def done (self, tag=None):
1413         """
1414         Stop decryption of the current object and finalize it with the active
1415         context. This will throw an *InvalidGCMTag* exception to indicate that
1416         the authentication tag does not match the data. If the tag is correct,
1417         the rest of the plaintext is returned.
1418         """
1419         data = b""
1420         try:
1421             if tag is None:
1422                 data = self.enc.finalize ()
1423             else:
1424                 if isinstance (tag, bytes) is False:
1425                     raise InvalidParameter ("done: wrong type of parameter "
1426                                             "tag: expected bytes, got %s"
1427                                             % type (tag))
1428                 data = self.enc.finalize_with_tag (self.tag)
1429         except cryptography.exceptions.InvalidTag:
1430             raise InvalidGCMTag ("done: tag mismatch of object %d: %s "
1431                                   "rejected by finalize ()"
1432                                   % (self.cnt, binascii.hexlify (self.tag)))
1433         self.ctsize += len (data)
1434         self.stats ["out"] += len (data)
1435         return data
1436
1437
1438     def process (self, buf):
1439         """
1440         Decrypt the bytes object *buf* with the active decryptor.
1441         """
1442         if isinstance (buf, bytes) is False:
1443             raise InvalidParameter ("process: expected byte buffer, not %s"
1444                                     % type (buf))
1445         self.ctsize += len (buf)
1446         data = super().process (buf)
1447         self.ptsize += len (data)
1448         return data
1449
1450
1451 ###############################################################################
1452 ## testing helpers
1453 ###############################################################################
1454
1455 def _patch_global (glob, vow, n=None):
1456     """
1457     Adapt upper file counter bound for testing IV logic. Completely unsafe.
1458     """
1459     assert vow == "I am fully aware that this will void my warranty."
1460     r = globals () [glob]
1461     if n is None:
1462         n = globals () [glob + "_DEFAULT"]
1463     globals () [glob] = n
1464     return r
1465
1466 _testing_set_AES_GCM_IV_CNT_MAX = \
1467         partial (_patch_global, "AES_GCM_IV_CNT_MAX")
1468
1469 _testing_set_PDTCRYPT_MAX_OBJ_SIZE = \
1470         partial (_patch_global, "PDTCRYPT_MAX_OBJ_SIZE")
1471
1472 def open2_dump_file (fname, dir_fd, force=False):
1473     outfd = -1
1474
1475     oflags = os.O_CREAT | os.O_WRONLY
1476     if force is True:
1477         oflags |= os.O_TRUNC
1478     else:
1479         oflags |= os.O_EXCL
1480
1481     try:
1482         outfd = os.open (fname, oflags,
1483                          stat.S_IRUSR | stat.S_IWUSR, dir_fd=dir_fd)
1484     except FileExistsError as exn:
1485         noise ("PDT: refusing to overwrite existing file %s" % fname)
1486         noise ("")
1487         raise RuntimeError ("destination file %s already exists" % fname)
1488     if PDTCRYPT_VERBOSE is True:
1489         noise ("PDT: new output file %s (fd=%d)" % (fname, outfd))
1490
1491     return outfd
1492
1493 ###############################################################################
1494 ## freestanding invocation
1495 ###############################################################################
1496
1497 PDTCRYPT_SUB_PROCESS = 0
1498 PDTCRYPT_SUB_SCRYPT  = 1
1499 PDTCRYPT_SUB_SCAN    = 2
1500
1501 PDTCRYPT_SUB = \
1502         { "process" : PDTCRYPT_SUB_PROCESS
1503         , "scrypt"  : PDTCRYPT_SUB_SCRYPT
1504         , "scan"    : PDTCRYPT_SUB_SCAN }
1505
1506 PDTCRYPT_DECRYPT   = 1 << 0 # decrypt archive with password
1507 PDTCRYPT_SPLIT     = 1 << 1 # split archive into individual objects
1508 PDTCRYPT_HASH      = 1 << 2 # output scrypt hash for file and given password
1509
1510 PDTCRYPT_SPLITNAME  = "pdtcrypt-object-%d.bin"
1511 PDTCRYPT_RESCUENAME = "pdtcrypt-rescue-object-%0.5d.bin"
1512
1513 PDTCRYPT_VERBOSE   = False
1514 PDTCRYPT_STRICTIVS = False
1515 PDTCRYPT_OVERWRITE = False
1516 PDTCRYPT_BLOCKSIZE = 1 << 12
1517 PDTCRYPT_SINK      = 0
1518 PDTCRYPT_SOURCE    = 1
1519 SELF               = None
1520
1521 PDTCRYPT_DEFAULT_VER  = 1
1522 PDTCRYPT_DEFAULT_PVER = 1
1523
1524 # scrypt hashing output control
1525 PDTCRYPT_SCRYPT_INTRANATOR = 0
1526 PDTCRYPT_SCRYPT_PARAMETERS = 1
1527 PDTCRYPT_SCRYPT_DEFAULT    = PDTCRYPT_SCRYPT_INTRANATOR
1528
1529 PDTCRYPT_SCRYPT_FORMAT = \
1530     { "i2n"    : PDTCRYPT_SCRYPT_INTRANATOR
1531     , "params" : PDTCRYPT_SCRYPT_PARAMETERS }
1532
1533 PDTCRYPT_TT_COLUMNS = 80 # assume standard terminal
1534
1535 class PDTDecryptionError (Exception):
1536     """Decryption failed."""
1537
1538 class PDTSplitError (Exception):
1539     """Decryption failed."""
1540
1541
1542 def noise (*a, **b):
1543     print (file=sys.stderr, *a, **b)
1544
1545
1546 class PassthroughDecryptor (object):
1547
1548     curhdr = None # write current header on first data write
1549
1550     def __init__ (self):
1551         if PDTCRYPT_VERBOSE is True:
1552             noise ("PDT: no encryption; data passthrough")
1553
1554     def next (self, hdr):
1555         ok, curhdr = hdr_make (hdr)
1556         if ok is False:
1557             raise PDTDecryptionError ("bad header %r" % hdr)
1558         self.curhdr = curhdr
1559
1560     def done (self):
1561         if self.curhdr is not None:
1562             return self.curhdr
1563         return b""
1564
1565     def process (self, d):
1566         if self.curhdr is not None:
1567             d = self.curhdr + d
1568             self.curhdr = None
1569         return d
1570
1571
1572 def depdtcrypt (mode, secret, ins, outs):
1573     """
1574     Remove PDTCRYPT layer from all objects encrypted with the secret. Used on a
1575     Deltatar backup this will yield a (possibly Gzip compressed) tarball.
1576     """
1577     ctleft     = -1              # length of ciphertext to consume
1578     ctcurrent  = 0               # total ciphertext of current object
1579     total_obj  = 0               # total number of objects read
1580     total_pt   = 0               # total plaintext bytes
1581     total_ct   = 0               # total ciphertext bytes
1582     total_read = 0               # total bytes read
1583     outfile    = None            # Python file object for output
1584
1585     if mode & PDTCRYPT_DECRYPT:  # decryptor
1586         ks = secret [0]
1587         if ks == PDTCRYPT_SECRET_PW:
1588             decr = Decrypt (password=secret [1], strict_ivs=PDTCRYPT_STRICTIVS)
1589         elif ks == PDTCRYPT_SECRET_KEY:
1590             key = secret [1]
1591             decr = Decrypt (key=key, strict_ivs=PDTCRYPT_STRICTIVS)
1592         else:
1593             raise InternalError ("‘%d’ does not specify a valid kind of secret"
1594                                  % ks)
1595     else:
1596         decr = PassthroughDecryptor ()
1597
1598     def nextout (_):
1599         """Dummy for non-split mode: output file does not vary."""
1600         return outs
1601
1602     if mode & PDTCRYPT_SPLIT:
1603         def nextout (outfile):
1604             """
1605             We were passed an fd as outs for accessing the destination
1606             directory where extracted archive components are supposed
1607             to end up in.
1608             """
1609
1610             if outfile is None:
1611                 if PDTCRYPT_VERBOSE is True:
1612                     noise ("PDT: no output file to close at this point")
1613             else:
1614                 if PDTCRYPT_VERBOSE is True:
1615                     noise ("PDT: release output file %r" % outfile)
1616                 # cleanup happens automatically by the GC; the next
1617                 # line will error out on account of an invalid fd
1618                 #outfile.close ()
1619
1620             assert total_obj > 0
1621             fname = PDTCRYPT_SPLITNAME % total_obj
1622             try:
1623                 outfd = open2_dump_file (fname, outs, force=PDTCRYPT_OVERWRITE)
1624             except RuntimeError as exn:
1625                 raise PDTSplitError (exn)
1626             return os.fdopen (outfd, "wb", closefd=True)
1627
1628
1629     def tell (s):
1630         """ESPIPE is normal on non-seekable stdio stream."""
1631         try:
1632             return s.tell ()
1633         except OSError as exn:
1634             if exn.errno == errno.ESPIPE:
1635                 return -1
1636
1637     def out (pt, outfile):
1638         npt = len (pt)
1639         nonlocal total_pt
1640         total_pt += npt
1641         if PDTCRYPT_VERBOSE is True:
1642             noise ("PDT:\t· decrypt plaintext %d B" % (npt))
1643         try:
1644             nn = outfile.write (pt)
1645         except OSError as exn: # probably ENOSPC
1646             raise DecryptionError ("error (%s)" % exn)
1647         if nn != npt:
1648             raise DecryptionError ("write aborted after %d of %d B" % (nn, npt))
1649
1650     while True:
1651         if ctleft <= 0:
1652             # current object completed; in a valid archive this marks either
1653             # the start of a new header or the end of the input
1654             if ctleft == 0: # current object requires finalization
1655                 if PDTCRYPT_VERBOSE is True:
1656                     noise ("PDT: %d finalize" % tell (ins))
1657                 try:
1658                     pt = decr.done ()
1659                 except InvalidGCMTag as exn:
1660                     raise DecryptionError ("error finalizing object %d (%d B): "
1661                                            "%r" % (total_obj, len (pt), exn)) \
1662                           from exn
1663                 out (pt, outfile)
1664                 if PDTCRYPT_VERBOSE is True:
1665                     noise ("PDT:\t· object validated")
1666
1667             if PDTCRYPT_VERBOSE is True:
1668                 noise ("PDT: %d hdr" % tell (ins))
1669             try:
1670                 hdr = hdr_read_stream (ins)
1671                 total_read += PDTCRYPT_HDR_SIZE
1672             except EndOfFile as exn:
1673                 total_read += exn.remainder
1674                 if total_ct + total_obj * PDTCRYPT_HDR_SIZE != total_read:
1675                     raise PDTDecryptionError ("ciphertext processed (%d B) plus "
1676                                               "overhead (%d × %d B) does not match "
1677                                               "the number of bytes read (%d )"
1678                                               % (total_ct, total_obj, PDTCRYPT_HDR_SIZE,
1679                                                  total_read))
1680                 # the single good exit
1681                 return total_read, total_obj, total_ct, total_pt
1682             except InvalidHeader as exn:
1683                 raise PDTDecryptionError ("invalid header at position %d in %r "
1684                                           "(%s)" % (tell (ins), exn, ins))
1685             if PDTCRYPT_VERBOSE is True:
1686                 pretty = hdr_fmt_pretty (hdr)
1687                 noise (reduce (lambda a, e: (a + "\n" if a else "") + "PDT:\t· " + e,
1688                                pretty.splitlines (), ""))
1689             ctcurrent = ctleft = hdr ["ctsize"]
1690
1691             decr.next (hdr)
1692
1693             total_obj += 1 # used in file counter with split mode
1694
1695             # finalization complete or skipped in case of first object in
1696             # stream; create a new output file if necessary
1697             outfile = nextout (outfile)
1698
1699             if PDTCRYPT_VERBOSE is True:
1700                 noise ("PDT: %d decrypt obj no. %d,  %d B"
1701                        % (tell (ins), total_obj, ctleft))
1702
1703         # always allocate a new buffer since python-cryptography doesn’t allow
1704         # passing a bytearray :/
1705         nexpect = min (ctleft, PDTCRYPT_BLOCKSIZE)
1706         if PDTCRYPT_VERBOSE is True:
1707             noise ("PDT:\t· [%d] %d%% done, read block (%d B of %d B remaining)"
1708                    % (tell (ins),
1709                       100 - ctleft * 100 / (ctcurrent > 0 and ctcurrent or 1),
1710                       nexpect, ctleft))
1711         ct      = ins.read (nexpect)
1712         nct     = len (ct)
1713         if nct < nexpect:
1714             off = tell (ins)
1715             raise EndOfFile (nct,
1716                              "hit EOF after %d of %d B in block [%d:%d); "
1717                              "%d B ciphertext remaining for object no %d"
1718                              % (nct, nexpect, off, off + nexpect, ctleft,
1719                                 total_obj))
1720         ctleft     -= nct
1721         total_ct   += nct
1722         total_read += nct
1723
1724         if PDTCRYPT_VERBOSE is True:
1725             noise ("PDT:\t· decrypt ciphertext %d B" % (nct))
1726         pt = decr.process (ct)
1727         out (pt, outfile)
1728
1729
1730 def deptdcrypt_mk_stream (kind, path):
1731     """Create stream from file or stdio descriptor."""
1732     if kind == PDTCRYPT_SINK:
1733         if path == "-":
1734             if PDTCRYPT_VERBOSE is True: noise ("PDT: sink: stdout")
1735             return sys.stdout.buffer
1736         else:
1737             if PDTCRYPT_VERBOSE is True: noise ("PDT: sink: file %s" % path)
1738             return io.FileIO (path, "w")
1739     if kind == PDTCRYPT_SOURCE:
1740         if path == "-":
1741             if PDTCRYPT_VERBOSE is True: noise ("PDT: source: stdin")
1742             return sys.stdin.buffer
1743         else:
1744             if PDTCRYPT_VERBOSE is True: noise ("PDT: source: file %s" % path)
1745             return io.FileIO (path, "r")
1746
1747     raise ValueError ("bogus stream “%s” / %s" % (kind, path))
1748
1749
1750 def mode_depdtcrypt (mode, secret, ins, outs):
1751     try:
1752         total_read, total_obj, total_ct, total_pt = \
1753             depdtcrypt (mode, secret, ins, outs)
1754     except DecryptionError as exn:
1755         noise ("PDT: Decryption failed:")
1756         noise ("PDT:")
1757         noise ("PDT:    “%s”" % exn)
1758         noise ("PDT:")
1759         noise ("PDT: Did you specify the correct key / password?")
1760         noise ("")
1761         return 1
1762     except PDTSplitError as exn:
1763         noise ("PDT: Split operation failed:")
1764         noise ("PDT:")
1765         noise ("PDT:    “%s”" % exn)
1766         noise ("PDT:")
1767         noise ("PDT: Hint: target directory should be empty.")
1768         noise ("")
1769         return 1
1770
1771     if PDTCRYPT_VERBOSE is True:
1772         noise ("PDT: decryption successful"                 )
1773         noise ("PDT:   %.10d bytes read"        % total_read)
1774         noise ("PDT:   %.10d objects decrypted" % total_obj )
1775         noise ("PDT:   %.10d bytes ciphertext"  % total_ct  )
1776         noise ("PDT:   %.10d bytes plaintext"   % total_pt  )
1777         noise (""                                           )
1778
1779     return 0
1780
1781
1782 def mode_scrypt (pw, ins=None, nacl=None, fmt=PDTCRYPT_SCRYPT_INTRANATOR):
1783     hsh = None
1784     paramversion = PDTCRYPT_DEFAULT_PVER
1785     if ins is not None:
1786         hsh, nacl, version, paramversion = scrypt_hashsource (pw, ins)
1787         defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
1788     else:
1789         nacl    = binascii.unhexlify (nacl)
1790         defs    = ENCRYPTION_PARAMETERS.get(paramversion, None)
1791         version = PDTCRYPT_DEFAULT_VER
1792
1793     kdfname, params = defs ["kdf"]
1794     if hsh is None:
1795         kdf = kdf_by_version (None, defs)
1796         hsh, _void = kdf (pw, nacl)
1797
1798     import json
1799
1800     if fmt == PDTCRYPT_SCRYPT_INTRANATOR:
1801         out = json.dumps ({ "salt"          : base64.b64encode (nacl).decode ()
1802                           , "key"           : base64.b64encode (hsh) .decode ()
1803                           , "paramversion"  : paramversion })
1804     elif fmt == PDTCRYPT_SCRYPT_PARAMETERS:
1805         out = json.dumps ({ "salt"          : binascii.hexlify (nacl).decode ()
1806                           , "key"           : binascii.hexlify (hsh) .decode ()
1807                           , "version"       : version
1808                           , "scrypt_params" : { "N"     : params ["N"]
1809                                               , "r"     : params ["r"]
1810                                               , "p"     : params ["p"]
1811                                               , "dkLen" : params ["dkLen"] } })
1812     else:
1813         raise RuntimeError ("bad scrypt output scheme %r" % fmt)
1814
1815     print (out)
1816
1817
1818 def noise_output_candidates (cands, indent=8, cols=PDTCRYPT_TT_COLUMNS):
1819     """
1820     Print a list of offsets without garbling the terminal too much.
1821
1822     The indent is counted from column zero; if it is wide enough, the “PDT: ”
1823     marker will be prepended, considered part of the indentation.
1824     """
1825     wd   = cols - 1
1826     nc   = len (cands)
1827     idt  = " " * indent if indent < 5 else "PDT: " + " " * (indent - 5)
1828     line = idt
1829     lpos = indent
1830     sep  = ","
1831     lsep = len (sep)
1832     init = True # prevent leading separator
1833
1834     if indent >= wd:
1835         raise ValueError ("the requested indentation exceeds the line "
1836                           "width by %d" % (indent - wd))
1837
1838     for n in cands:
1839         ns  = "%d" % n
1840         lns = len (ns)
1841         if init is False:
1842             line += sep
1843             lpos += lsep
1844
1845         lpos += lns
1846         if lpos > wd: # line break
1847             noise (line)
1848             line = idt
1849             lpos = indent + lns
1850         elif init is True:
1851             init = False
1852         else: # space
1853             line += ' '
1854             lpos += 1
1855
1856         line += ns
1857
1858     if lpos != indent:
1859         noise (line)
1860
1861
1862 SLICE_START = 1 # ordering is important to have starts of intervals
1863 SLICE_END   = 0 # sorted before equal ends
1864
1865 def find_overlaps (slices):
1866     """
1867     Find overlapping slices: iterate open/close points of intervals, tracking
1868     the ones open at any time.
1869     """
1870     bounds = []
1871     inside = set () # of indices into bounds
1872     ovrlp  = set () # of indices into bounds
1873
1874     for i, s in enumerate (slices):
1875         bounds.append ((s [0], SLICE_START, i))
1876         bounds.append ((s [1], SLICE_END  , i))
1877     bounds = sorted (bounds)
1878
1879     for val in bounds:
1880         i = val [2]
1881         if val [1] == SLICE_START:
1882             inside.add (i)
1883         else:
1884             if len (inside) > 1: # closing one that overlapped
1885                 ovrlp |= inside
1886             inside.remove (i)
1887
1888     return [ slices [i] for i in ovrlp ]
1889
1890
1891 def mode_scan (secret, fname, outs=None, nacl=None):
1892     """
1893     Dissect a binary file, looking for PDTCRYPT headers and objects.
1894
1895     If *outs* is supplied, recoverable data will be dumped into the specified
1896     directory.
1897     """
1898     try:
1899         ifd = os.open (fname, os.O_RDONLY)
1900     except FileNotFoundError:
1901         noise ("PDT: failed to open %s readonly" % fname)
1902         noise ("")
1903         usage (err=True)
1904
1905     try:
1906         if PDTCRYPT_VERBOSE is True:
1907             noise ("PDT: scan for potential sync points")
1908         cands = locate_hdr_candidates (ifd)
1909         if len (cands) == 0:
1910             noise ("PDT: scan complete: input does not contain potential PDT "
1911                    "headers; giving up.")
1912             return -1
1913         if PDTCRYPT_VERBOSE is True:
1914             noise ("PDT: scan complete: found %d candidates:" % len (cands))
1915             noise_output_candidates (cands)
1916     except:
1917         os.close (ifd)
1918         raise
1919
1920     junk, todo, slices = [], [], []
1921     try:
1922         nobj = 0
1923         for cand in cands:
1924             nobj += 1
1925             vdt, hdr = inspect_hdr (ifd, cand)
1926
1927             vdts = verdict_fmt (vdt)
1928
1929             if vdt == HDR_CAND_JUNK:
1930                 noise ("PDT: obj %d: %s object: bad header, skipping" % vdts)
1931                 junk.append (cand)
1932             else:
1933                 off0 = cand + PDTCRYPT_HDR_SIZE
1934                 if PDTCRYPT_VERBOSE is True:
1935                     noise ("PDT: obj %d: read payload @%d" % (nobj, off0))
1936                     pretty = hdr_fmt_pretty (hdr)
1937                     noise (reduce (lambda a, e: (a + "\n" if a else "") + "PDT:\t· " + e,
1938                                 pretty.splitlines (), ""))
1939
1940                 ofd = -1
1941                 if outs is not None:
1942                     ofname = PDTCRYPT_RESCUENAME % nobj
1943                     ofd = open2_dump_file (ofname, outs, force=PDTCRYPT_OVERWRITE)
1944
1945                 ctsize = hdr ["ctsize"]
1946                 try:
1947                     l = try_decrypt (ifd, off0, hdr, secret, ofd=ofd)
1948                     ok = l == ctsize
1949                     slices.append ((off0, off0 + l))
1950                 finally:
1951                     if ofd != -1:
1952                         os.close (ofd)
1953                 if vdt == HDR_CAND_GOOD and ok is True:
1954                     noise ("PDT: %d → ✓ %s object %d–%d"
1955                            % (cand, vdts, off0, off0 + ctsize))
1956                 elif vdt == HDR_CAND_FISHY and ok is True:
1957                     noise ("PDT: %d → × %s object %d–%d, corrupt header"
1958                            % (cand, vdts, off0, off0 + ctsize))
1959                 elif vdt == HDR_CAND_GOOD and ok is False:
1960                     noise ("PDT: %d → × %s object %d–%d, problematic payload"
1961                            % (cand, vdts, off0, off0 + ctsize))
1962                 elif vdt == HDR_CAND_FISHY and ok is False:
1963                     noise ("PDT: %d → × %s object %d–%d, corrupt header, problematic "
1964                            "ciphertext" % (cand, vdts, off0, off0 + ctsize))
1965                 else:
1966                     raise Unreachable
1967     finally:
1968         os.close (ifd)
1969
1970     if len (junk) == 0:
1971         noise ("PDT: all headers ok")
1972     else:
1973         noise ("PDT: %d candidates not parseable as headers:" % len (junk))
1974         noise_output_candidates (junk)
1975
1976     overlap = find_overlaps (slices)
1977     if len (overlap) > 0:
1978         noise ("PDT: %d objects overlapping others" % len (overlap))
1979         for slice in overlap:
1980             noise ("PDT:    × %d→%d" % (slice [0], slice [1]))
1981
1982 def usage (err=False):
1983     out = print
1984     if err is True:
1985         out = noise
1986     indent = ' ' * len (SELF)
1987     out ("usage: %s SUBCOMMAND { --help" % SELF)
1988     out ("       %s            | [ -v ] { -p PASSWORD | -k KEY }" % indent)
1989     out ("       %s              [ { -i | --in }  { - | SOURCE } ]" % indent)
1990     out ("       %s              [ { -n | --nacl } { SALT } ]" % indent)
1991     out ("       %s              [ { -o | --out } { - | DESTINATION } ]" % indent)
1992     out ("       %s              [ -D | --no-decrypt ] [ -S | --split ]" % indent)
1993     out ("       %s              [ -f | --format ]" % indent)
1994     out ("")
1995     out ("\twhere")
1996     out ("\t\tSUBCOMMAND      main mode: { process | scrypt }")
1997     out ("\t\t                where:")
1998     out ("\t\t                   process: extract objects from PDT archive")
1999     out ("\t\t                   scrypt:  calculate hash from password and first object")
2000     out ("\t\t-p PASSWORD     password to derive the encryption key from")
2001     out ("\t\t-k KEY          encryption key as 16 bytes in hexadecimal notation")
2002     out ("\t\t-s              enforce strict handling of initialization vectors")
2003     out ("\t\t-i SOURCE       file name to read from")
2004     out ("\t\t-o DESTINATION  file to write output to")
2005     out ("\t\t-n SALT         provide salt for scrypt mode in hex encoding")
2006     out ("\t\t-v              print extra info")
2007     out ("\t\t-S              split into files at object boundaries; this")
2008     out ("\t\t                requires DESTINATION to refer to directory")
2009     out ("\t\t-D              PDT header and ciphertext passthrough")
2010     out ("\t\t-f              format of SCRYPT hash output (“default” or “parameters”)")
2011     out ("")
2012     out ("\tinstead of filenames, “-” may used to specify stdin / stdout")
2013     out ("")
2014     sys.exit ((err is True) and 42 or 0)
2015
2016
2017 def bail (msg):
2018     noise (msg)
2019     noise ("")
2020     usage (err=True)
2021     raise Unreachable
2022
2023
2024 def parse_argv (argv):
2025     global PDTCRYPT_OVERWRITE
2026     global SELF
2027     mode          = PDTCRYPT_DECRYPT
2028     secret        = None
2029     insspec       = None
2030     outsspec      = None
2031     outs          = None
2032     nacl          = None
2033     scrypt_format = PDTCRYPT_SCRYPT_DEFAULT
2034
2035     argvi = iter (argv)
2036     SELF  = os.path.basename (next (argvi))
2037
2038     try:
2039         rawsubcmd  = next (argvi)
2040         subcommand = PDTCRYPT_SUB [rawsubcmd]
2041     except StopIteration:
2042         bail ("ERROR: subcommand required")
2043     except KeyError:
2044         bail ("ERROR: invalid subcommand “%s” specified" % rawsubcmd)
2045
2046     def checked_arg ():
2047         nonlocal argvi
2048         try:
2049             return next (argvi)
2050         except StopIteration:
2051             bail ("ERROR: argument list incomplete")
2052
2053     def checked_secret (s):
2054         nonlocal secret
2055         if secret is None:
2056             secret = s
2057         else:
2058             bail ("ERROR: encountered “%s” but secret already given" % arg)
2059
2060     for arg in argvi:
2061         if arg in [ "-h", "--help" ]:
2062             usage ()
2063             raise Unreachable
2064         elif arg in [ "-v", "--verbose", "--wtf" ]:
2065             global PDTCRYPT_VERBOSE
2066             PDTCRYPT_VERBOSE = True
2067         elif arg in [ "-i", "--in", "--source" ]:
2068             insspec = checked_arg ()
2069             if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt from %s" % insspec)
2070         elif arg in [ "-p", "--password" ]:
2071             arg = checked_arg ()
2072             checked_secret (make_secret (password=arg))
2073             if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypting with password")
2074         else:
2075             if subcommand == PDTCRYPT_SUB_PROCESS:
2076                 if arg in [ "-s", "--strict-ivs" ]:
2077                     global PDTCRYPT_STRICTIVS
2078                     PDTCRYPT_STRICTIVS = True
2079                 elif arg in [ "-o", "--out", "--dest", "--sink" ]:
2080                     outsspec = checked_arg ()
2081                     if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
2082                 elif arg in [ "-f", "--force" ]:
2083                     PDTCRYPT_OVERWRITE = True
2084                     if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
2085                 elif arg in [ "-S", "--split" ]:
2086                     mode |= PDTCRYPT_SPLIT
2087                     if PDTCRYPT_VERBOSE is True: noise ("PDT: split files")
2088                 elif arg in [ "-D", "--no-decrypt" ]:
2089                     mode &= ~PDTCRYPT_DECRYPT
2090                     if PDTCRYPT_VERBOSE is True: noise ("PDT: not decrypting")
2091                 elif arg in [ "-k", "--key" ]:
2092                     arg = checked_arg ()
2093                     checked_secret (make_secret (key=arg))
2094                     if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypting with key")
2095                 else:
2096                     bail ("ERROR: unexpected positional argument “%s”" % arg)
2097             elif subcommand == PDTCRYPT_SUB_SCRYPT:
2098                 if arg in [ "-n", "--nacl", "--salt" ]:
2099                     nacl = checked_arg ()
2100                     if PDTCRYPT_VERBOSE is True: noise ("PDT: salt key with %s" % nacl)
2101                 elif arg in [ "-f", "--format" ]:
2102                     arg = checked_arg ()
2103                     try:
2104                         scrypt_format = PDTCRYPT_SCRYPT_FORMAT [arg]
2105                     except KeyError:
2106                         bail ("ERROR: invalid scrypt output format %s" % arg)
2107                     if PDTCRYPT_VERBOSE is True:
2108                         noise ("PDT: scrypt output format “%s”" % scrypt_format)
2109                 else:
2110                     bail ("ERROR: unexpected positional argument “%s”" % arg)
2111             elif subcommand == PDTCRYPT_SUB_SCAN:
2112                 if arg in [ "-o", "--out", "--dest", "--sink" ]:
2113                     outsspec = checked_arg ()
2114                     if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
2115                 elif arg in [ "-f", "--force" ]:
2116                     PDTCRYPT_OVERWRITE = True
2117                     if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
2118                 else:
2119                     bail ("ERROR: unexpected positional argument “%s”" % arg)
2120
2121     if secret is None:
2122         if PDTCRYPT_VERBOSE is True:
2123             noise ("ERROR: no password or key specified, trying $PDTCRYPT_PASSWORD")
2124         epw = os.getenv ("PDTCRYPT_PASSWORD")
2125         if epw is not None:
2126             checked_secret (make_secret (password=epw.strip ()))
2127
2128     if secret is None:
2129         if PDTCRYPT_VERBOSE is True:
2130             noise ("ERROR: no password or key specified, trying $PDTCRYPT_KEY")
2131         ek = os.getenv ("PDTCRYPT_KEY")
2132         if ek is not None:
2133             checked_secret (make_secret (key=ek.strip ()))
2134
2135     if secret is None:
2136         if subcommand == PDTCRYPT_SUB_SCRYPT:
2137             bail ("ERROR: scrypt hash mode requested but no password given")
2138         elif mode & PDTCRYPT_DECRYPT:
2139             bail ("ERROR: decryption requested but no password given")
2140
2141     if mode & PDTCRYPT_SPLIT and outsspec is None:
2142         bail ("ERROR: split mode is incompatible with stdout sink "
2143               "(the default)")
2144
2145     if subcommand == PDTCRYPT_SUB_SCAN and outsspec is None:
2146         pass # no output by default in scan mode
2147     elif mode & PDTCRYPT_SPLIT or subcommand == PDTCRYPT_SUB_SCAN:
2148         # destination must be directory
2149         if outsspec == "-":
2150             bail ("ERROR: mode is incompatible with stdout sink")
2151         try:
2152             try:
2153                 os.makedirs (outsspec, 0o700)
2154             except FileExistsError:
2155                 # if it’s a directory with appropriate perms, everything is
2156                 # good; otherwise, below invocation of open(2) will fail
2157                 pass
2158             outs = os.open (outsspec, os.O_DIRECTORY, 0o600)
2159         except FileNotFoundError as exn:
2160             bail ("ERROR: cannot create target directory “%s”" % outsspec)
2161         except NotADirectoryError as exn:
2162             bail ("ERROR: target path “%s” is not a directory" % outsspec)
2163     else:
2164         outs = deptdcrypt_mk_stream (PDTCRYPT_SINK, outsspec or "-")
2165
2166     if subcommand == PDTCRYPT_SUB_SCAN:
2167         if insspec is None:
2168             bail ("ERROR: please supply an input file for scanning")
2169         if insspec == '-':
2170             bail ("ERROR: input must be seekable; please specify a file")
2171         return True, partial (mode_scan, secret, insspec, outs, nacl=nacl)
2172
2173     if subcommand == PDTCRYPT_SUB_SCRYPT:
2174         if secret [0] == PDTCRYPT_SECRET_KEY:
2175             bail ("ERROR: scrypt mode requires a password")
2176         if     insspec is not None and nacl is not None \
2177             or insspec is     None and nacl is     None :
2178                 bail ("ERROR: please supply either an input file or "
2179                       "the salt")
2180
2181     # default to stdout
2182     ins = None
2183     if insspec is not None or subcommand != PDTCRYPT_SUB_SCRYPT:
2184         ins = deptdcrypt_mk_stream (PDTCRYPT_SOURCE, insspec or "-")
2185
2186     if subcommand == PDTCRYPT_SUB_SCRYPT:
2187         return True, partial (mode_scrypt, secret [1].encode (), ins, nacl,
2188                               fmt=scrypt_format)
2189
2190     return True, partial (mode_depdtcrypt, mode, secret, ins, outs)
2191
2192
2193 def main (argv):
2194     ok, runner = parse_argv (argv)
2195
2196     if ok is True: return runner ()
2197
2198     return 1
2199
2200
2201 if __name__ == "__main__":
2202     sys.exit (main (sys.argv))
2203