930d1f9c229ad0d8688111b157a507c82c14d1ec
[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 Trouble with python-cryptography packages: authentication tags can only be
28 passed in advance: https://github.com/pyca/cryptography/pull/3421
29
30 Errors
31 -------------------------------------------------------------------------------
32
33 Errors fall into roughly three categories:
34
35     - Cryptographical errors or invalid data.
36
37         - ``InvalidGCMTag`` (decryption failed on account of an invalid GCM
38           tag),
39         - ``InvalidIVFixedPart`` (IV fixed part of object not found in list),
40         - ``DuplicateIV`` (the IV of an encrypted object already occurred),
41         - ``DecryptionError`` (used in CLI decryption for presenting error
42           conditions to the user).
43
44     - Incorrect usage of the library.
45
46         - ``InvalidParameter`` (non-conforming user supplied parameter),
47         - ``InvalidHeader`` (data passed for reading not parsable into header),
48         - ``FormatError`` (cannot handle header or parameter version),
49         - ``RuntimeError``.
50
51     - Bad internal state. If one of these is encountered it means that a state
52       was reached that shouldn’t occur during normal processing.
53
54         - ``InternalError``,
55         - ``Unreachable``.
56
57 Also, ``EndOfFile`` is used as a sentinel to communicate that a stream supplied
58 for reading is exhausted.
59
60 Initialization Vectors
61 -------------------------------------------------------------------------------
62
63 Initialization vectors are checked reuse during the lifetime of a decryptor.
64 The fixed counters for metadata files cannot be reused and attempts to do so
65 will cause a DuplicateIV error. This means the length of objects encrypted with
66 a metadata counter is capped at 63 GB.
67
68 For ordinary, non-metadata payload, there is an optional mode with strict IV
69 checking that causes a crypto context to fail if an IV encountered or created
70 was already used for decrypting or encrypting, respectively, an earlier object.
71 Note that this mode can trigger false positives when decrypting non-linearly,
72 e. g. when traversing the same object multiple times. Since the crypto context
73 has no notion of a position in a PDT encrypted archive, this condition must be
74 sorted out downstream.
75
76 Command Line Utility
77 -------------------------------------------------------------------------------
78
79 ``crypto.py`` may be invoked as a script for decrypting, validating, and
80 splitting PDT encrypted files. Consult the usage message for details.
81
82 Usage examples:
83
84 Decrypt from stdin using the password ‘foo’: ::
85
86     $ crypto.py process foo -i - -o - <some-file.tar.gz.pdtcrypt >some-file.tar.gz
87
88 Output verbose information about the encrypted objects in the archive: ::
89
90     $ crypto.py process foo -v -i some-file.tar.gz.pdtcrypt -o /dev/null
91     PDT: decrypt from some-file.tar.gz.pdtcrypt
92     PDT: decrypt to /dev/null
93     PDT: source: file some-file.tar.gz.pdtcrypt
94     PDT: sink: file /dev/null
95     PDT: 0 hdr
96     PDT:    · version         = 1                    : 0100
97     PDT:    · paramversion    = 1                    : 0100
98     PDT:    · nacl                                   : d270 b031 00d1 87e2 c946 610d 7b7f 7e5f
99     PDT:    · iv                                     : 02ee 3dd7 a963 1eb1 0100 0000
100     PDT:    · ctsize          = 591                  : 4f02 0000 0000 0000
101     PDT:    · tag                                    : 5b2d 6d8b 8f82 4842 12fd 0b10 b6e3 369b
102     PDT: 64 decrypt obj no. 1,  591 B
103     PDT:    · [64] 0% done, read block (591 B of 591 B remaining)
104     PDT:    · decrypt ciphertext 591 B
105     PDT:    · decrypt plaintext 591 B
106     PDT: 655 finalize
107     …
108
109 Also, the mode *scrypt* allows deriving encryption keys. To calculate the
110 encryption key from the password ‘foo’ and the salt of the first object in a
111 PDT encrypted file: ::
112
113     $ crypto.py scrypt foo -i some-file.pdtcrypt
114     {"paramversion": 1, "salt": "Cqzbk48e3peEjzWto8D0yA==", "key": "JH9EkMwaM4x9F5aim5gK/Q=="}
115
116 The computed 16 byte key is given in hexadecimal notation in the value to
117 ``hash`` and can be fed into Python’s ``binascii.unhexlify()`` to obtain the
118 corresponding binary representation.
119
120 Note that in Scrypt hashing mode, no data integrity checks are being performed.
121 If the wrong password is given, a wrong key will be derived. Whether the password
122 was indeed correct can only be determined by decrypting. Note that since PDT
123 archives essentially consist of a stream of independent objects, the salt and
124 other parameters may change. Thus a key derived using above method from the
125 first object doesn’t necessarily apply to any of the subsequent objects.
126
127 """
128
129 import base64
130 import binascii
131 import bisect
132 import ctypes
133 import io
134 from functools import reduce, partial
135 import os
136 import struct
137 import sys
138 import time
139 import types
140 try:
141     import enum34
142 except ImportError as exn:
143     pass
144
145 if __name__ == "__main__": ## Work around the import mechanism’s 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_GCM_MAX_SIZE              = (1 << 36) - (1 << 5) # 2^39 - 2^8 b ≅ 64 GB
317 PDTCRYPT_MAX_OBJ_SIZE_DEFAULT = 63 * (1 << 30)       #                63 GB
318 PDTCRYPT_MAX_OBJ_SIZE         = PDTCRYPT_MAX_OBJ_SIZE_DEFAULT
319
320 # index and info files are written on-the fly while encrypting so their
321 # counters must be available inadvance
322 AES_GCM_IV_CNT_INFOFILE     = 1 # constant
323 AES_GCM_IV_CNT_INDEX        = AES_GCM_IV_CNT_INFOFILE + 1
324 AES_GCM_IV_CNT_DATA         = AES_GCM_IV_CNT_INDEX    + 1 # also for multivolume
325 AES_GCM_IV_CNT_MAX_DEFAULT  = 0xffFFffFF
326 AES_GCM_IV_CNT_MAX          = AES_GCM_IV_CNT_MAX_DEFAULT
327
328 # IV structure and generation
329 PDTCRYPT_IV_GEN_MAX_RETRIES = 10 # ×
330 PDTCRYPT_IV_FIXEDPART_SIZE  =  8 # B
331 PDTCRYPT_IV_COUNTER_SIZE    =  4 # B
332
333 ###############################################################################
334 ## header, trailer
335 ###############################################################################
336 #
337 # Interface:
338 #
339 #    struct hdrinfo
340 #      { version      : u16
341 #      , paramversion : u16
342 #      , nacl         : [u8; 16]
343 #      , iv           : [u8; 12]
344 #      , ctsize       : usize
345 #      , tag          : [u8; 16] }
346 #
347 #    fn hdr_read (f : handle) -> hdrinfo;
348 #    fn hdr_make (f : handle, h : hdrinfo) -> IOResult<usize>;
349 #    fn hdr_fmt (h : hdrinfo) -> String;
350 #
351
352 def hdr_read (data):
353     """
354     Read bytes as header structure.
355
356     If the input could not be interpreted as a header, fail with
357     ``InvalidHeader``.
358     """
359
360     try:
361         mag, version, paramversion, nacl, iv, ctsize, tag = \
362             struct.unpack (FMT_I2N_HDR, data)
363     except Exception as exn:
364         raise InvalidHeader ("error unpacking header from [%r]: %s"
365                              % (binascii.hexlify (data), str (exn)))
366
367     if mag != PDTCRYPT_HDR_MAGIC:
368         raise InvalidHeader ("bad magic in header: expected [%s], got [%s]"
369                              % (PDTCRYPT_HDR_MAGIC, mag))
370
371     return \
372         {      "version" : version
373         , "paramversion" : paramversion
374         ,         "nacl" : nacl
375         ,           "iv" : iv
376         ,       "ctsize" : ctsize
377         ,          "tag" : tag
378         }
379
380
381 def hdr_read_stream (instr):
382     """
383     Read header from stream at the current position.
384
385     Fail with ``InvalidHeader`` if insufficient bytes were read from the
386     stream, or if the content could not be interpreted as a header.
387     """
388     data = instr.read(PDTCRYPT_HDR_SIZE)
389     ldata = len (data)
390     if ldata == 0:
391         raise EndOfFile
392     elif ldata != PDTCRYPT_HDR_SIZE:
393         raise InvalidHeader ("hdr_read_stream: expected %d B, received %d B"
394                              % (PDTCRYPT_HDR_SIZE, ldata))
395     return hdr_read (data)
396
397
398 def hdr_from_params (version, paramversion, nacl, iv, ctsize, tag):
399     """
400     Assemble the necessary values into a PDTCRYPT header.
401
402     :type      version: int to fit   uint16_t
403     :type paramversion: int to fit   uint16_t
404     :type         nacl: bytes to fit uint8_t[16]
405     :type           iv: bytes to fit uint8_t[12]
406     :type         size: int to fit   uint64_t
407     :type          tag: bytes to fit uint8_t[16]
408     """
409     buf  = bytearray (PDTCRYPT_HDR_SIZE)
410     bufv = memoryview (buf)
411
412     try:
413         struct.pack_into (FMT_I2N_HDR, bufv, 0,
414                           PDTCRYPT_HDR_MAGIC,
415                           version, paramversion, nacl, iv, ctsize, tag)
416     except Exception as exn:
417         return False, "error assembling header: %s" % str (exn)
418
419     return True, bytes (buf)
420
421
422 def hdr_make_dummy (s):
423     """
424     Create a header sized block of bytes initialized to a value derived from a
425     string. Used to verify we’ve jumped back correctly to the actual position
426     of the object header.
427     """
428     c = reduce (lambda a, c: a + ord(c), s, 0) % 0xFF
429     return bytes (bytearray (struct.pack ("B", c)) * PDTCRYPT_HDR_SIZE)
430
431
432 def hdr_make (hdr):
433     """
434     Assemble a header from the given header structure.
435     """
436     return hdr_from_params (version=hdr.get("version"),
437                             paramversion=hdr.get("paramversion"),
438                             nacl=hdr.get("nacl"), iv=hdr.get("iv"),
439                             ctsize=hdr.get("ctsize"), tag=hdr.get("tag"))
440
441
442 HDR_FMT = "I2n_header { version: %d, paramversion: %d, nacl: %s[%d]," \
443                       " iv: %s[%d], ctsize: %d, tag: %s[%d] }"
444
445 def hdr_fmt (h):
446     """Format a header structure into readable output."""
447     return HDR_FMT % (h["version"], h["paramversion"],
448                       binascii.hexlify (h["nacl"]), len(h["nacl"]),
449                       binascii.hexlify (h["iv"]), len(h["iv"]),
450                       h["ctsize"],
451                       binascii.hexlify (h["tag"]), len(h["tag"]))
452
453
454 def hex_spaced_of_bytes (b):
455     """Format bytes object, hexdump style."""
456     return " ".join ([ "%.2x%.2x" % (c1, c2)
457                        for c1, c2 in zip (b[0::2], b[1::2]) ]) \
458          + (len (b) | 1 == len (b) and " %.2x" % b[-1] or "") # odd lengths
459
460
461 def hdr_iv_counter (h):
462     """Extract the variable part of the IV of the given header."""
463     _fixed, cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
464     return cnt
465
466
467 def hdr_iv_fixed (h):
468     """Extract the fixed part of the IV of the given header."""
469     fixed, _cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
470     return fixed
471
472
473 hdr_dump = hex_spaced_of_bytes
474
475
476 HDR_FMT_PRETTY = \
477 """version         = %-4d                 : %s
478 paramversion    = %-4d                 : %s
479 nacl                                   : %s
480 iv                                     : %s
481 ctsize          = %-20d : %s
482 tag                                    : %s
483 """
484
485 def hdr_fmt_pretty (h):
486     """
487     Format header structure into multi-line representation of its contents and
488     their raw representation. (Omit the implicit “PDTCRYPT” magic bytes that
489     precede every header.)
490     """
491     return HDR_FMT_PRETTY \
492                 % (h["version"],
493                    hex_spaced_of_bytes (struct.pack (FMT_UINT16_LE, h["version"])),
494                    h["paramversion"],
495                    hex_spaced_of_bytes (struct.pack (FMT_UINT16_LE, h["paramversion"])),
496                    hex_spaced_of_bytes (h["nacl"]),
497                    hex_spaced_of_bytes (h["iv"]),
498                    h["ctsize"],
499                    hex_spaced_of_bytes (struct.pack (FMT_UINT64_LE, h["ctsize"])),
500                    hex_spaced_of_bytes (h["tag"]))
501
502 IV_FMT = "((f %s) (c %d))"
503
504 def iv_fmt (iv):
505     """Format the two components of an IV in a readable fashion."""
506     fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
507     return IV_FMT % (binascii.hexlify (fixed), cnt)
508
509
510 ###############################################################################
511 ## passthrough / null encryption
512 ###############################################################################
513
514 class PassthroughCipher (object):
515
516     tag = struct.pack ("<QQ", 0, 0)
517
518     def __init__          (self)    : pass
519
520     def update            (self, b) : return b
521
522     def finalize          (self)    : return b""
523
524     def finalize_with_tag (self, _) : return b""
525
526 ###############################################################################
527 ## convenience wrapper
528 ###############################################################################
529
530
531 def kdf_dummy (klen, password, _nacl):
532     """
533     Fake KDF for testing purposes that is called when parameter version zero is
534     encountered.
535     """
536     q, r = divmod (klen, len (password))
537     if isinstance (password, bytes) is False:
538         password = password.encode ()
539     return password * q + password [:r], b""
540
541
542 SCRYPT_KEY_MEMO = { } # static because needed for both the info file and the archive
543
544
545 def kdf_scrypt (params, password, nacl):
546     """
547     Wrapper for the Scrypt KDF, corresponds to parameter version one. The
548     computation result is memoized based on the inputs to facilitate spawning
549     multiple encryption contexts.
550     """
551     N = params["N"]
552     r = params["r"]
553     p = params["p"]
554     dkLen = params["dkLen"]
555
556     if nacl is None:
557         nacl = os.urandom (params["NaCl_LEN"])
558
559     key_parms = (password, nacl, N, r, p, dkLen)
560     global SCRYPT_KEY_MEMO
561     if key_parms not in SCRYPT_KEY_MEMO:
562         SCRYPT_KEY_MEMO [key_parms] = \
563             pylibscrypt.scrypt (password, nacl, N, r, p, dkLen)
564     return SCRYPT_KEY_MEMO [key_parms], nacl
565
566
567 def kdf_by_version (paramversion=None, defs=None):
568     """
569     Pick the KDF handler corresponding to the parameter version or the
570     definition set.
571
572     :rtype: function (password : str, nacl : str) -> str
573     """
574     if paramversion is not None:
575         defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
576     if defs is None:
577         raise InvalidParameter ("no encryption parameters for version %r"
578                                 % paramversion)
579     (kdf, params) = defs["kdf"]
580     fn = None
581     if kdf == "scrypt" : fn = kdf_scrypt
582     if kdf == "dummy"  : fn = kdf_dummy
583     if fn is None:
584         raise ValueError ("key derivation method %r unknown" % kdf)
585     return partial (fn, params)
586
587
588 ###############################################################################
589 ## SCRYPT hashing
590 ###############################################################################
591
592 def scrypt_hashsource (pw, ins):
593     """
594     Calculate the SCRYPT hash from the password and the information contained
595     in the first header found in ``ins``.
596
597     This does not validate whether the first object is encrypted correctly.
598     """
599     if isinstance (pw, str) is True:
600         pw = str.encode (pw)
601     elif isinstance (pw, bytes) is False:
602         raise InvalidParameter ("password must be a string, not %s"
603                                 % type (password))
604     if isinstance (ins, io.BufferedReader) is False and \
605             isinstance (ins, io.FileIO) is False:
606         raise InvalidParameter ("file to hash must be opened in “binary” mode")
607     hdr = None
608     try:
609         hdr = hdr_read_stream (ins)
610     except EndOfFile as exn:
611         noise ("PDT: malformed input: end of file reading first object header")
612         noise ("PDT:")
613         return 1
614
615     nacl = hdr ["nacl"]
616     pver = hdr ["paramversion"]
617     if PDTCRYPT_VERBOSE is True:
618         noise ("PDT: salt of first object          : %s" % binascii.hexlify (nacl))
619         noise ("PDT: parameter version of archive  : %d" % pver)
620
621     try:
622         defs = ENCRYPTION_PARAMETERS.get(pver, None)
623         kdfname, params = defs ["kdf"]
624         if kdfname != "scrypt":
625             noise ("PDT: input is not an SCRYPT archive")
626             noise ("")
627             return 1
628         kdf = kdf_by_version (None, defs)
629     except ValueError as exn:
630         noise ("PDT: object has unknown parameter version %d" % pver)
631
632     hsh, _void = kdf (pw, nacl)
633
634     return hsh, nacl, hdr ["version"], pver
635
636
637 def scrypt_hashfile (pw, fname):
638     """
639     Calculate the SCRYPT hash from the password and the information contained
640     in the first header found in the given file. The header is read only at
641     offset zero.
642     """
643     with deptdcrypt_mk_stream (PDTCRYPT_SOURCE, fname  or "-") as ins:
644         hsh, _void, _void, _void = scrypt_hashsource (pw, ins)
645         return hsh
646
647
648 ###############################################################################
649 ## AES-GCM context
650 ###############################################################################
651
652 class Crypto (object):
653     """
654     Encryption context to remain alive throughout an entire tarfile pass.
655     """
656     enc  = None
657     nacl = None
658     key  = None
659     cnt  = None # file counter (uint32_t != 0)
660     iv   = None # current IV
661     fixed        = None  # accu for 64 bit fixed parts of IV
662     used_ivs     = None  # tracks IVs
663     strict_ivs   = False # if True, panic on duplicate object IV
664     password     = None
665     paramversion = None
666     stats = { "in"  : 0
667             , "out" : 0
668             , "obj" : 0 }
669
670     ctsize  = -1
671     ptsize  = -1
672     info_counter_used  = False
673     index_counter_used = False
674
675     def __init__ (self, *al, **akv):
676         self.used_ivs = set ()
677         self.set_parameters (*al, **akv)
678
679
680     def next_fixed (self):
681         # NOP for decryption
682         pass
683
684
685     def set_object_counter (self, cnt=None):
686         """
687         Safely set the internal counter of encrypted objects. Numerous
688         constraints apply:
689
690         The same counter may not be reused in combination with one IV fixed
691         part. This is validated elsewhere in the IV handling.
692
693         Counter zero is invalid. The first two counters are reserved for
694         metadata. The implementation does not allow for splitting metadata
695         files over multiple encrypted objects. (This would be possible by
696         assigning new fixed parts.) Thus in a Deltatar backup there is at most
697         one object with a counter value of one and two. On creation of a
698         context, the initial counter may be chosen. The globals
699         ``AES_GCM_IV_CNT_INFOFILE`` and ``AES_GCM_IV_CNT_INDEX`` can be used to
700         request one of the reserved values. If one of these values has been
701         used, any further attempt of setting the counter to that value will
702         be rejected with an ``InvalidFileCounter`` exception.
703
704         Out of bounds values (i. e. below one and more than the maximum of 2³²)
705         cause an ``InvalidParameter`` exception to be thrown.
706         """
707         if cnt is None:
708             self.cnt = AES_GCM_IV_CNT_DATA
709             return
710         if cnt == 0 or cnt > AES_GCM_IV_CNT_MAX + 1:
711             raise InvalidParameter ("invalid counter value %d requested: "
712                                     "acceptable values are from 1 to %d"
713                                     % (cnt, AES_GCM_IV_CNT_MAX))
714         if cnt == AES_GCM_IV_CNT_INFOFILE:
715             if self.info_counter_used is True:
716                 raise InvalidFileCounter ("attempted to reuse info file "
717                                           "counter %d: must be unique" % cnt)
718             self.info_counter_used = True
719         elif cnt == AES_GCM_IV_CNT_INDEX:
720             if self.index_counter_used is True:
721                 raise InvalidFileCounter ("attempted to reuse index file "
722                                           " counter %d: must be unique" % cnt)
723             self.index_counter_used = True
724         if cnt <= AES_GCM_IV_CNT_MAX:
725             self.cnt = cnt
726             return
727         # cnt == AES_GCM_IV_CNT_MAX + 1 → wrap
728         self.cnt = AES_GCM_IV_CNT_DATA
729         self.next_fixed ()
730
731
732     def set_parameters (self, password=None, key=None, paramversion=None,
733                         nacl=None, counter=None, strict_ivs=False):
734         """
735         Configure the internal state of a crypto context. Not intended for
736         external use.
737         """
738         self.next_fixed ()
739         self.set_object_counter (counter)
740         self.strict_ivs = strict_ivs
741
742         if paramversion is not None:
743             self.paramversion = paramversion
744
745         if key is not None:
746             self.key, self.nacl = key, nacl
747             return
748
749         if password is not None:
750             if isinstance (password, bytes) is False:
751                 password = str.encode (password)
752             self.password = password
753             if paramversion is None and nacl is None:
754                 # postpone key setup until first header is available
755                 return
756             kdf = kdf_by_version (paramversion)
757             if kdf is not None:
758                 self.key, self.nacl = kdf (password, nacl)
759
760
761     def process (self, buf):
762         """
763         Encrypt / decrypt a buffer. Invokes the ``.update()`` method on the
764         wrapped encryptor or decryptor, respectively.
765
766         The Cryptography exception ``AlreadyFinalized`` is translated to an
767         ``InternalError`` at this point. It may occur in sound code when the GC
768         closes an encrypting stream after an error. Everywhere else it must be
769         treated as a bug.
770         """
771         if self.enc is None:
772             raise RuntimeError ("process: context not initialized")
773         self.stats ["in"] += len (buf)
774         try:
775             out = self.enc.update (buf)
776         except cryptography.exceptions.AlreadyFinalized as exn:
777             raise InternalError (exn)
778         self.stats ["out"] += len (out)
779         return out
780
781
782     def next (self, password, paramversion, nacl, iv):
783         """
784         Prepare for encrypting another object: Reset the data counters and
785         change the configuration in case one of the variable parameters differs
786         from the last object. Also check the IV for duplicates and error out
787         if strict checking was requested.
788         """
789         self.ctsize = 0
790         self.ptsize = 0
791         self.stats ["obj"] += 1
792
793         self.check_duplicate_iv (iv)
794
795         if (   self.paramversion != paramversion
796             or self.password     != password
797             or self.nacl         != nacl):
798             self.set_parameters (password=password, paramversion=paramversion,
799                                  nacl=nacl, strict_ivs=self.strict_ivs)
800
801
802     def check_duplicate_iv (self, iv):
803         """
804         Add an IV (the 12 byte representation as in the header) to the list. With
805         strict checking enabled, this will throw a ``DuplicateIV``. Depending on
806         the context, this may indicate a serious error (IV reuse).
807         """
808         if self.strict_ivs is True and iv in self.used_ivs:
809             raise DuplicateIV ("iv %s was reused" % iv_fmt (iv))
810         # vi has not been used before; add to collection
811         self.used_ivs.add (iv)
812
813
814     def counters (self):
815         """
816         Access the data counters.
817         """
818         return self.stats ["obj"], self.stats ["in"], self.stats ["out"]
819
820
821 class Encrypt (Crypto):
822
823     lastinfo     = None
824     version      = None
825     paramenc     = None
826
827     def __init__ (self, version, paramversion, password=None, key=None, nacl=None,
828                   counter=AES_GCM_IV_CNT_DATA, strict_ivs=True):
829         """
830         The ctor will throw immediately if one of the parameters does not conform
831         to our expectations.
832
833                   counter=AES_GCM_IV_CNT_DATA, strict_ivs=True):
834         :type      version: int to fit   uint16_t
835         :type paramversion: int to fit   uint16_t
836         :param    password: mutually exclusive with ``key``
837         :type     password: bytes
838         :param         key: mutually exclusive with ``password``
839         :type          key: bytes
840         :type         nacl: bytes
841         :type      counter: initial object counter the values
842                             ``AES_GCM_IV_CNT_INFOFILE`` and
843                             ``AES_GCM_IV_CNT_INDEX`` are unique in each backup set
844                             and cannot be reused even with different fixed parts.
845         :type   strict_ivs: bool
846         """
847         if         password is     None and key is     None \
848                 or password is not None and key is not None :
849             raise InvalidParameter ("__init__: need either key or password")
850
851         if key is not None:
852             if isinstance (key, bytes) is False:
853                 raise InvalidParameter ("__init__: key must be provided as "
854                                         "bytes, not %s" % type (key))
855             if nacl is None:
856                 raise InvalidParameter ("__init__: salt must be provided along "
857                                         "with encryption key")
858         else: # password, no key
859             if isinstance (password, str) is False:
860                 raise InvalidParameter ("__init__: password must be a string, not %s"
861                                         % type (password))
862             if len (password) == 0:
863                 raise InvalidParameter ("__init__: supplied empty password but not "
864                                         "permitted for PDT encrypted files")
865         # version
866         if isinstance (version, int) is False:
867             raise InvalidParameter ("__init__: version number must be an "
868                                     "integer, not %s" % type (version))
869         if version < 0:
870             raise InvalidParameter ("__init__: version number must be a "
871                                     "nonnegative integer, not %d" % version)
872         # paramversion
873         if isinstance (paramversion, int) is False:
874             raise InvalidParameter ("__init__: crypto parameter version number "
875                                     "must be an integer, not %s"
876                                     % type (paramversion))
877         if paramversion < 0:
878             raise InvalidParameter ("__init__: crypto parameter version number "
879                                     "must be a nonnegative integer, not %d"
880                                     % paramversion)
881         # salt
882         if nacl is not None:
883             if isinstance (nacl, bytes) is False:
884                 raise InvalidParameter ("__init__: salt given, but of type %s "
885                                         "instead of bytes" % type (nacl))
886             # salt length would depend on the actual encryption so it can’t be
887             # validated at this point
888         self.fixed        = [ ]
889         self.version      = version
890         self.paramenc     = ENCRYPTION_PARAMETERS.get (paramversion) ["enc"]
891
892         super().__init__ (password, key, paramversion, nacl, counter=counter,
893                           strict_ivs=strict_ivs)
894
895
896     def next_fixed (self, retries=PDTCRYPT_IV_GEN_MAX_RETRIES):
897         """
898         Generate the next IV fixed part by reading eight bytes from
899         ``/dev/urandom``. The buffer so obtained is tested against the fixed
900         parts used so far to prevent accidental reuse of IVs. After a
901         configurable number of attempts to create a unique fixed part, it will
902         refuse to continue with an ``IVFixedPartError``. This is unlikely to
903         ever happen on a normal system but may detect an issue with the random
904         generator.
905
906         The list of fixed parts that were used by the context at hand can be
907         accessed through the ``.fixed`` list. Its last element is the fixed
908         part currently in use.
909         """
910         i = 0
911         while i < retries:
912             fp = os.urandom (PDTCRYPT_IV_FIXEDPART_SIZE)
913             if fp not in self.fixed:
914                 self.fixed.append (fp)
915                 return
916             i += 1
917         raise IVFixedPartError ("error obtaining a unique IV fixed part from "
918                                 "/dev/urandom; giving up after %d tries" % i)
919
920
921     def iv_make (self):
922         """
923         Construct a 12-bytes IV from the current fixed part and the object
924         counter.
925         """
926         return struct.pack(FMT_I2N_IV, self.fixed [-1], self.cnt)
927
928
929     def next (self, filename=None, counter=None):
930         """
931         Prepare for encrypting the next incoming object. Update the counter
932         and put together the IV, possibly changing prefixes. Then create the
933         new encryptor.
934
935         The argument ``counter`` can be used to specify a file counter for this
936         object. Unless it is one of the reserved values, the counter of
937         subsequent objects will be computed from this one.
938
939         If this is the first object in a series, ``filename`` is required,
940         otherwise it is reused if not present. The value is used to derive a
941         header sized placeholder to use until after encryption when all the
942         inputs to construct the final header are available. This is then
943         matched in ``.done()`` against the value found at the position of the
944         header. The motivation for this extra check is primarily to assist
945         format debugging: It makes stray headers easy to spot in malformed
946         PDTCRYPT files.
947         """
948         if filename is None:
949             if self.lastinfo is None:
950                 raise InvalidParameter ("next: filename is mandatory for "
951                                         "first object")
952             filename, _dummy = self.lastinfo
953         else:
954             if isinstance (filename, str) is False:
955                 raise InvalidParameter ("next: filename must be a string, no %s"
956                                         % type (filename))
957         if counter is not None:
958             if isinstance (counter, int) is False:
959                 raise InvalidParameter ("next: the supplied counter is of "
960                                         "invalid type %s; please pass an "
961                                         "integer instead" % type (counter))
962             self.set_object_counter (counter)
963
964         self.iv = self.iv_make ()
965         if self.paramenc == "aes-gcm":
966             self.enc = Cipher \
967                             ( algorithms.AES (self.key)
968                             , modes.GCM (self.iv)
969                             , backend = default_backend ()) \
970                             .encryptor ()
971         elif self.paramenc == "passthrough":
972             self.enc = PassthroughCipher ()
973         else:
974             raise InvalidParameter ("next: parameter version %d not known"
975                                     % self.paramversion)
976         hdrdum = hdr_make_dummy (filename)
977         self.lastinfo = (filename, hdrdum)
978         super().next (self.password, self.paramversion, self.nacl, self.iv)
979
980         self.set_object_counter (self.cnt + 1)
981         return hdrdum
982
983
984     def done (self, cmpdata):
985         """
986         Complete encryption of an object. After this has been called, attempts
987         of encrypting further data will cause an error until ``.next()`` is
988         invoked properly.
989
990         Returns a 64 bytes buffer containing the object header including all
991         values including the “late” ones e. g. the ciphertext size and the
992         GCM tag.
993         """
994         if isinstance (cmpdata, bytes) is False:
995             raise InvalidParameter ("done: comparison input expected as bytes, "
996                                     "not %s" % type (cmpdata))
997         if self.lastinfo is None:
998             raise RuntimeError ("done: encryption context not initialized")
999         filename, hdrdum = self.lastinfo
1000         if cmpdata != hdrdum:
1001             raise RuntimeError ("done: bad sync of header for object %d: "
1002                                 "preliminary data does not match; this likely "
1003                                 "indicates a wrongly repositioned stream"
1004                                 % self.cnt)
1005         data = self.enc.finalize ()
1006         self.stats ["out"] += len (data)
1007         self.ctsize += len (data)
1008         ok, hdr = hdr_from_params (self.version, self.paramversion, self.nacl,
1009                                    self.iv, self.ctsize, self.enc.tag)
1010         if ok is False:
1011             raise InternalError ("error constructing header: %r" % hdr)
1012         return data, hdr, self.fixed
1013
1014
1015     def process (self, buf):
1016         """
1017         Encrypt a chunk of plaintext with the active encryptor. Returns the
1018         size of the input consumed. This **must** be checked downstream. If the
1019         maximum possible object size has been reached, the current context must
1020         be finalized and a new one established before any further data can be
1021         encrypted. The second argument is the remainder of the plaintext that
1022         was not encrypted for the caller to use immediately after the new
1023         context is ready.
1024         """
1025         if isinstance (buf, bytes) is False:
1026             raise InvalidParameter ("process: expected byte buffer, not %s"
1027                                     % type (buf))
1028         bsize = len (buf)
1029         newptsize = self.ptsize + bsize
1030         diff = newptsize - PDTCRYPT_MAX_OBJ_SIZE
1031         if diff > 0:
1032             bsize -= diff
1033             newptsize = PDTCRYPT_MAX_OBJ_SIZE
1034         self.ptsize = newptsize
1035         data = super().process (buf [:bsize])
1036         self.ctsize += len (data)
1037         return bsize, data
1038
1039
1040 class Decrypt (Crypto):
1041
1042     tag        = None   # GCM tag, part of header
1043     last_iv    = None   # check consecutive ivs in strict mode
1044
1045     def __init__ (self, password=None, key=None, counter=None, fixedparts=None,
1046                   strict_ivs=False):
1047         """
1048         Sanitizing ctor for the decryption context. ``fixedparts`` specifies a
1049         list of IV fixed parts accepted during decryption. If a fixed part is
1050         encountered that is not in the list, decryption will fail.
1051
1052         :param    password: mutually exclusive with ``key``
1053         :type     password: bytes
1054         :param         key: mutually exclusive with ``password``
1055         :type          key: bytes
1056         :type      counter: initial object counter the values
1057                             ``AES_GCM_IV_CNT_INFOFILE`` and
1058                             ``AES_GCM_IV_CNT_INDEX`` are unique in each backup set
1059                             and cannot be reused even with different fixed parts.
1060         :type   fixedparts: bytes list
1061         """
1062         if         password is     None and key is     None \
1063                 or password is not None and key is not None :
1064             raise InvalidParameter ("__init__: need either key or password")
1065
1066         if key is not None:
1067             if isinstance (key, bytes) is False:
1068                 raise InvalidParameter ("__init__: key must be provided as "
1069                                         "bytes, not %s" % type (key))
1070         else: # password, no key
1071             if isinstance (password, str) is False:
1072                 raise InvalidParameter ("__init__: password must be a string, not %s"
1073                                         % type (password))
1074             if len (password) == 0:
1075                 raise InvalidParameter ("__init__: supplied empty password but not "
1076                                         "permitted for PDT encrypted files")
1077         # fixed parts
1078         if fixedparts is not None:
1079             if isinstance (fixedparts, list) is False:
1080                 raise InvalidParameter ("__init__: IV fixed parts must be "
1081                                         "supplied as list, not %s"
1082                                         % type (fixedparts))
1083             self.fixed = fixedparts
1084             self.fixed.sort ()
1085
1086         super().__init__ (password=password, key=key, counter=counter,
1087                           strict_ivs=strict_ivs)
1088
1089
1090     def valid_fixed_part (self, iv):
1091         """
1092         Check if a fixed part was already seen.
1093         """
1094         # check if fixed part is known
1095         fixed, _cnt = struct.unpack (FMT_I2N_IV, iv)
1096         i = bisect.bisect_left (self.fixed, fixed)
1097         return i != len (self.fixed) and self.fixed [i] == fixed
1098
1099
1100     def check_consecutive_iv (self, iv):
1101         """
1102         Check whether the counter part of the given IV is indeed the successor
1103         of the currently present counter. This should always be the case for
1104         the objects in a well formed PDT archive but should not be enforced
1105         when decrypting out-of-order.
1106         """
1107         fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
1108         if self.strict_ivs is True \
1109                 and self.last_iv is not None \
1110                 and self.last_iv [0] == fixed \
1111                 and self.last_iv [1] != cnt - 1:
1112             raise NonConsecutiveIV ("iv %s counter not successor of "
1113                                     "last object (expected %d, found %d)"
1114                                     % (iv_fmt (self.last_iv [1]), cnt))
1115         self.last_iv = (iv, cnt)
1116
1117
1118     def next (self, hdr):
1119         """
1120         Start decrypting the next object. The PDTCRYPT header for the object
1121         can be given either as already parsed object or as bytes.
1122         """
1123         if isinstance (hdr, bytes) is True:
1124             hdr = hdr_read (hdr)
1125         elif isinstance (hdr, dict) is False:
1126             # this won’t catch malformed specs though
1127             raise InvalidParameter ("next: wrong type of parameter hdr: "
1128                                     "expected bytes or spec, got %s"
1129                                     % type (hdr))
1130         try:
1131             paramversion = hdr ["paramversion"]
1132             nacl         = hdr ["nacl"]
1133             iv           = hdr ["iv"]
1134             tag          = hdr ["tag"]
1135         except KeyError:
1136             raise InvalidHeader ("next: not a header %r" % hdr)
1137
1138         super().next (self.password, paramversion, nacl, iv)
1139         if self.fixed is not None and self.valid_fixed_part (iv) is False:
1140             raise InvalidIVFixedPart ("iv %s has invalid fixed part"
1141                                       % iv_fmt (iv))
1142         self.check_consecutive_iv (iv)
1143
1144         self.tag = tag
1145         defs = ENCRYPTION_PARAMETERS.get (paramversion, None)
1146         if defs is None:
1147             raise FormatError ("header contains unknown parameter version %d; "
1148                                "maybe the file was created by a more recent "
1149                                "version of Deltatar" % paramversion)
1150         enc = defs ["enc"]
1151         if enc == "aes-gcm":
1152             self.enc = Cipher \
1153                             ( algorithms.AES (self.key)
1154                             , modes.GCM (iv, tag=self.tag)
1155                             , backend = default_backend ()) \
1156                             . decryptor ()
1157         elif enc == "passthrough":
1158             self.enc = PassthroughCipher ()
1159         else:
1160             raise InternalError ("encryption parameter set %d refers to unknown "
1161                                  "mode %r" % (paramversion, enc))
1162         self.set_object_counter (self.cnt + 1)
1163
1164
1165     def done (self, tag=None):
1166         """
1167         Stop decryption of the current object and finalize it with the active
1168         context. This will throw an *InvalidGCMTag* exception to indicate that
1169         the authentication tag does not match the data. If the tag is correct,
1170         the rest of the plaintext is returned.
1171         """
1172         data = b""
1173         try:
1174             if tag is None:
1175                 data = self.enc.finalize ()
1176             else:
1177                 if isinstance (tag, bytes) is False:
1178                     raise InvalidParameter ("done: wrong type of parameter "
1179                                             "tag: expected bytes, got %s"
1180                                             % type (tag))
1181                 data = self.enc.finalize_with_tag (self.tag)
1182         except cryptography.exceptions.InvalidTag:
1183             raise InvalidGCMTag ("done: tag mismatch of object %d: %s "
1184                                   "rejected by finalize ()"
1185                                   % (self.cnt, binascii.hexlify (self.tag)))
1186         self.ctsize += len (data)
1187         self.stats ["out"] += len (data)
1188         return data
1189
1190
1191     def process (self, buf):
1192         """
1193         Decrypt the bytes object *buf* with the active decryptor.
1194         """
1195         if isinstance (buf, bytes) is False:
1196             raise InvalidParameter ("process: expected byte buffer, not %s"
1197                                     % type (buf))
1198         self.ctsize += len (buf)
1199         data = super().process (buf)
1200         self.ptsize += len (data)
1201         return data
1202
1203
1204 ###############################################################################
1205 ## testing helpers
1206 ###############################################################################
1207
1208 def _patch_global (glob, vow, n=None):
1209     """
1210     Adapt upper file counter bound for testing IV logic. Completely unsafe.
1211     """
1212     assert vow == "I am fully aware that this will void my warranty."
1213     r = globals () [glob]
1214     if n is None:
1215         n = globals () [glob + "_DEFAULT"]
1216     globals () [glob] = n
1217     return r
1218
1219 _testing_set_AES_GCM_IV_CNT_MAX = \
1220         partial (_patch_global, "AES_GCM_IV_CNT_MAX")
1221
1222 _testing_set_PDTCRYPT_MAX_OBJ_SIZE = \
1223         partial (_patch_global, "PDTCRYPT_MAX_OBJ_SIZE")
1224
1225 ###############################################################################
1226 ## freestanding invocation
1227 ###############################################################################
1228
1229 PDTCRYPT_SUB_PROCESS = 0
1230 PDTCRYPT_SUB_SCRYPT  = 1
1231
1232 PDTCRYPT_SUB = \
1233         { "process" : PDTCRYPT_SUB_PROCESS
1234         , "scrypt"  : PDTCRYPT_SUB_SCRYPT }
1235
1236 PDTCRYPT_SECRET_PW   = 0
1237 PDTCRYPT_SECRET_KEY  = 1
1238
1239 PDTCRYPT_DECRYPT   = 1 << 0 # decrypt archive with password
1240 PDTCRYPT_SPLIT     = 1 << 1 # split archive into individual objects
1241 PDTCRYPT_HASH      = 1 << 2 # output scrypt hash for file and given password
1242
1243 PDTCRYPT_SPLITNAME = "pdtcrypt-object-%d.bin"
1244
1245 PDTCRYPT_VERBOSE   = False
1246 PDTCRYPT_STRICTIVS = False
1247 PDTCRYPT_OVERWRITE = False
1248 PDTCRYPT_BLOCKSIZE = 1 << 12
1249 PDTCRYPT_SINK      = 0
1250 PDTCRYPT_SOURCE    = 1
1251 SELF               = None
1252
1253 PDTCRYPT_DEFAULT_VER  = 1
1254 PDTCRYPT_DEFAULT_PVER = 1
1255
1256 # scrypt hashing output control
1257 PDTCRYPT_SCRYPT_INTRANATOR = 0
1258 PDTCRYPT_SCRYPT_PARAMETERS = 1
1259 PDTCRYPT_SCRYPT_DEFAULT    = PDTCRYPT_SCRYPT_INTRANATOR
1260
1261 PDTCRYPT_SCRYPT_FORMAT = \
1262     { "i2n"    : PDTCRYPT_SCRYPT_INTRANATOR
1263     , "params" : PDTCRYPT_SCRYPT_PARAMETERS }
1264
1265
1266 class PDTDecryptionError (Exception):
1267     """Decryption failed."""
1268
1269 class PDTSplitError (Exception):
1270     """Decryption failed."""
1271
1272
1273 def noise (*a, **b):
1274     print (file=sys.stderr, *a, **b)
1275
1276
1277 class PassthroughDecryptor (object):
1278
1279     curhdr = None # write current header on first data write
1280
1281     def __init__ (self):
1282         if PDTCRYPT_VERBOSE is True:
1283             noise ("PDT: no encryption; data passthrough")
1284
1285     def next (self, hdr):
1286         ok, curhdr = hdr_make (hdr)
1287         if ok is False:
1288             raise PDTDecryptionError ("bad header %r" % hdr)
1289         self.curhdr = curhdr
1290
1291     def done (self):
1292         if self.curhdr is not None:
1293             return self.curhdr
1294         return b""
1295
1296     def process (self, d):
1297         if self.curhdr is not None:
1298             d = self.curhdr + d
1299             self.curhdr = None
1300         return d
1301
1302
1303 def depdtcrypt (mode, secret, ins, outs):
1304     """
1305     Remove PDTCRYPT layer from all objects encrypted with the secret. Used on a
1306     Deltatar backup this will yield a (possibly Gzip compressed) tarball.
1307     """
1308     ctleft     = -1              # length of ciphertext to consume
1309     ctcurrent  = 0               # total ciphertext of current object
1310     total_obj  = 0               # total number of objects read
1311     total_pt   = 0               # total plaintext bytes
1312     total_ct   = 0               # total ciphertext bytes
1313     total_read = 0               # total bytes read
1314     outfile    = None            # Python file object for output
1315
1316     if mode & PDTCRYPT_DECRYPT:  # decryptor
1317         ks = secret [0]
1318         if ks == PDTCRYPT_SECRET_PW:
1319             decr = Decrypt (password=secret [1], strict_ivs=PDTCRYPT_STRICTIVS)
1320         elif ks == PDTCRYPT_SECRET_KEY:
1321             key = binascii.unhexlify (secret [1])
1322             decr = Decrypt (key=key, strict_ivs=PDTCRYPT_STRICTIVS)
1323         else:
1324             raise InternalError ("‘%d’ does not specify a valid kind of secret"
1325                                  % ks)
1326     else:
1327         decr = PassthroughDecryptor ()
1328
1329     def nextout (_):
1330         """Dummy for non-split mode: output file does not vary."""
1331         return outs
1332
1333     if mode & PDTCRYPT_SPLIT:
1334         def nextout (outfile):
1335             """
1336             We were passed an fd as outs for accessing the destination
1337             directory where extracted archive components are supposed
1338             to end up in.
1339             """
1340
1341             if outfile is None:
1342                 if PDTCRYPT_VERBOSE is True:
1343                     noise ("PDT: no output file to close at this point")
1344                 else:
1345                     if PDTCRYPT_VERBOSE is True:
1346                         noise ("PDT: release output file %r" % outfile)
1347                     # cleanup happens automatically by the GC; the next
1348                     # line will error out on account of an invalid fd
1349                     #outfile.close ()
1350
1351             assert total_obj > 0
1352             fname = PDTCRYPT_SPLITNAME % total_obj
1353             try:
1354                 oflags = os.O_CREAT | os.O_WRONLY
1355                 if PDTCRYPT_OVERWRITE is True:
1356                     oflags |= os.O_TRUNC
1357                 else:
1358                     oflags |= os.O_EXCL
1359                 outfd = os.open (fname, oflags, 0o600, dir_fd=outs)
1360                 if PDTCRYPT_VERBOSE is True:
1361                     noise ("PDT: new output file %s → %d" % (fname, outfd))
1362             except FileExistsError as exn:
1363                 noise ("PDT: refusing to overwrite existing file %s" % fname)
1364                 noise ("")
1365                 raise PDTSplitError ("destination file %s already exists"
1366                                      % fname)
1367
1368             return os.fdopen (outfd, "wb", closefd=True)
1369
1370
1371     def tell (s):
1372         """ESPIPE is normal on stdio stream."""
1373         try:
1374             return s.tell ()
1375         except OSError as exn:
1376             if exn.errno == 29:
1377                 return -1
1378
1379     def out (pt, outfile):
1380         npt = len (pt)
1381         nonlocal total_pt
1382         total_pt += npt
1383         if PDTCRYPT_VERBOSE is True:
1384             noise ("PDT:\t· decrypt plaintext %d B" % (npt))
1385         try:
1386             nn = outfile.write (pt)
1387         except OSError as exn: # probably ENOSPC
1388             raise DecryptionError ("error (%s)" % exn)
1389         if nn != npt:
1390             raise DecryptionError ("write aborted after %d of %d B" % (nn, npt))
1391
1392     while True:
1393         if ctleft <= 0:
1394             # current object completed; in a valid archive this marks either
1395             # the start of a new header or the end of the input
1396             if ctleft == 0: # current object requires finalization
1397                 if PDTCRYPT_VERBOSE is True:
1398                     noise ("PDT: %d finalize" % tell (ins))
1399                 try:
1400                     pt = decr.done ()
1401                 except InvalidGCMTag as exn:
1402                     raise DecryptionError ("error finalizing object %d (%d B): "
1403                                            "%r" % (total_obj, len (pt), exn)) \
1404                           from exn
1405                 out (pt, outfile)
1406                 if PDTCRYPT_VERBOSE is True:
1407                     noise ("PDT:\t· object validated")
1408
1409             if PDTCRYPT_VERBOSE is True:
1410                 noise ("PDT: %d hdr" % tell (ins))
1411             try:
1412                 hdr = hdr_read_stream (ins)
1413                 total_read += PDTCRYPT_HDR_SIZE
1414             except EndOfFile as exn:
1415                 total_read += exn.remainder
1416                 if total_ct + total_obj * PDTCRYPT_HDR_SIZE != total_read:
1417                     raise PDTDecryptionError ("ciphertext processed (%d B) plus "
1418                                               "overhead (%d × %d B) does not match "
1419                                               "the number of bytes read (%d )"
1420                                               % (total_ct, total_obj, PDTCRYPT_HDR_SIZE,
1421                                                  total_read))
1422                 # the single good exit
1423                 return total_read, total_obj, total_ct, total_pt
1424             except InvalidHeader as exn:
1425                 raise PDTDecryptionError ("invalid header at position %d in %r "
1426                                           "(%s)" % (tell (ins), exn, ins))
1427             if PDTCRYPT_VERBOSE is True:
1428                 pretty = hdr_fmt_pretty (hdr)
1429                 noise (reduce (lambda a, e: (a + "\n" if a else "") + "PDT:\t· " + e,
1430                                pretty.splitlines (), ""))
1431             ctcurrent = ctleft = hdr ["ctsize"]
1432
1433             decr.next (hdr)
1434
1435             total_obj += 1 # used in file counter with split mode
1436
1437             # finalization complete or skipped in case of first object in
1438             # stream; create a new output file if necessary
1439             outfile = nextout (outfile)
1440
1441             if PDTCRYPT_VERBOSE is True:
1442                 noise ("PDT: %d decrypt obj no. %d,  %d B"
1443                        % (tell (ins), total_obj, ctleft))
1444
1445         # always allocate a new buffer since python-cryptography doesn’t allow
1446         # passing a bytearray :/
1447         nexpect = min (ctleft, PDTCRYPT_BLOCKSIZE)
1448         if PDTCRYPT_VERBOSE is True:
1449             noise ("PDT:\t· [%d] %d%% done, read block (%d B of %d B remaining)"
1450                    % (tell (ins),
1451                       100 - ctleft * 100 / (ctcurrent > 0 and ctcurrent or 1),
1452                       nexpect, ctleft))
1453         ct      = ins.read (nexpect)
1454         nct     = len (ct)
1455         if nct < nexpect:
1456             off = tell (ins)
1457             raise EndOfFile (nct,
1458                              "hit EOF after %d of %d B in block [%d:%d); "
1459                              "%d B ciphertext remaining for object no %d"
1460                              % (nct, nexpect, off, off + nexpect, ctleft,
1461                                 total_obj))
1462         ctleft     -= nct
1463         total_ct   += nct
1464         total_read += nct
1465
1466         if PDTCRYPT_VERBOSE is True:
1467             noise ("PDT:\t· decrypt ciphertext %d B" % (nct))
1468         pt = decr.process (ct)
1469         out (pt, outfile)
1470
1471
1472 def deptdcrypt_mk_stream (kind, path):
1473     """Create stream from file or stdio descriptor."""
1474     if kind == PDTCRYPT_SINK:
1475         if path == "-":
1476             if PDTCRYPT_VERBOSE is True: noise ("PDT: sink: stdout")
1477             return sys.stdout.buffer
1478         else:
1479             if PDTCRYPT_VERBOSE is True: noise ("PDT: sink: file %s" % path)
1480             return io.FileIO (path, "w")
1481     if kind == PDTCRYPT_SOURCE:
1482         if path == "-":
1483             if PDTCRYPT_VERBOSE is True: noise ("PDT: source: stdin")
1484             return sys.stdin.buffer
1485         else:
1486             if PDTCRYPT_VERBOSE is True: noise ("PDT: source: file %s" % path)
1487             return io.FileIO (path, "r")
1488
1489     raise ValueError ("bogus stream “%s” / %s" % (kind, path))
1490
1491
1492 def mode_depdtcrypt (mode, secret, ins, outs):
1493     try:
1494         total_read, total_obj, total_ct, total_pt = \
1495             depdtcrypt (mode, secret, ins, outs)
1496     except DecryptionError as exn:
1497         noise ("PDT: Decryption failed:")
1498         noise ("PDT:")
1499         noise ("PDT:    “%s”" % exn)
1500         noise ("PDT:")
1501         noise ("PDT: Did you specify the correct key / password?")
1502         noise ("")
1503         return 1
1504     except PDTSplitError as exn:
1505         noise ("PDT: Split operation failed:")
1506         noise ("PDT:")
1507         noise ("PDT:    “%s”" % exn)
1508         noise ("PDT:")
1509         noise ("PDT: Hint: target directory should be empty.")
1510         noise ("")
1511         return 1
1512
1513     if PDTCRYPT_VERBOSE is True:
1514         noise ("PDT: decryption successful"                 )
1515         noise ("PDT:   %.10d bytes read"        % total_read)
1516         noise ("PDT:   %.10d objects decrypted" % total_obj )
1517         noise ("PDT:   %.10d bytes ciphertext"  % total_ct  )
1518         noise ("PDT:   %.10d bytes plaintext"   % total_pt  )
1519         noise (""                                           )
1520
1521     return 0
1522
1523
1524 def mode_scrypt (pw, ins=None, nacl=None, fmt=PDTCRYPT_SCRYPT_INTRANATOR):
1525     hsh = None
1526     paramversion = PDTCRYPT_DEFAULT_PVER
1527     if ins is not None:
1528         hsh, nacl, version, paramversion = scrypt_hashsource (pw, ins)
1529         defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
1530     else:
1531         nacl    = binascii.unhexlify (nacl)
1532         defs    = ENCRYPTION_PARAMETERS.get(paramversion, None)
1533         version = PDTCRYPT_DEFAULT_VER
1534
1535     kdfname, params = defs ["kdf"]
1536     if hsh is None:
1537         kdf = kdf_by_version (None, defs)
1538         hsh, _void = kdf (pw, nacl)
1539
1540     import json
1541
1542     if fmt == PDTCRYPT_SCRYPT_INTRANATOR:
1543         out = json.dumps ({ "salt"          : base64.b64encode (nacl).decode ()
1544                           , "key"           : base64.b64encode (hsh) .decode ()
1545                           , "paramversion"  : paramversion })
1546     elif fmt == PDTCRYPT_SCRYPT_PARAMETERS:
1547         out = json.dumps ({ "salt"          : binascii.hexlify (nacl).decode ()
1548                           , "key"           : binascii.hexlify (hsh) .decode ()
1549                           , "version"       : version
1550                           , "scrypt_params" : { "N"     : params ["N"]
1551                                               , "r"     : params ["r"]
1552                                               , "p"     : params ["p"]
1553                                               , "dkLen" : params ["dkLen"] } })
1554     else:
1555         raise RuntimeError ("bad scrypt output scheme %r" % fmt)
1556
1557     print (out)
1558
1559
1560
1561 def usage (err=False):
1562     out = print
1563     if err is True:
1564         out = noise
1565     indent = ' ' * len (SELF)
1566     out ("usage: %s SUBCOMMAND { --help" % SELF)
1567     out ("       %s            | [ -v ] { -p PASSWORD | -k KEY }" % indent)
1568     out ("       %s              [ { -i | --in }  { - | SOURCE } ]" % indent)
1569     out ("       %s              [ { -n | --nacl } { SALT } ]" % indent)
1570     out ("       %s              [ { -o | --out } { - | DESTINATION } ]" % indent)
1571     out ("       %s              [ -D | --no-decrypt ] [ -S | --split ]" % indent)
1572     out ("       %s              [ -f | --format ]" % indent)
1573     out ("")
1574     out ("\twhere")
1575     out ("\t\tSUBCOMMAND      main mode: { process | scrypt }")
1576     out ("\t\t                where:")
1577     out ("\t\t                   process: extract objects from PDT archive")
1578     out ("\t\t                   scrypt:  calculate hash from password and first object")
1579     out ("\t\t-p PASSWORD     password to derive the encryption key from")
1580     out ("\t\t-k KEY          encryption key as 16 bytes in hexadecimal notation")
1581     out ("\t\t-s              enforce strict handling of initialization vectors")
1582     out ("\t\t-i SOURCE       file name to read from")
1583     out ("\t\t-o DESTINATION  file to write output to")
1584     out ("\t\t-n SALT         provide salt for scrypt mode in hex encoding")
1585     out ("\t\t-v              print extra info")
1586     out ("\t\t-S              split into files at object boundaries; this")
1587     out ("\t\t                requires DESTINATION to refer to directory")
1588     out ("\t\t-D              PDT header and ciphertext passthrough")
1589     out ("\t\t-f              format of SCRYPT hash output (“default” or “parameters”)")
1590     out ("")
1591     out ("\tinstead of filenames, “-” may used to specify stdin / stdout")
1592     out ("")
1593     sys.exit ((err is True) and 42 or 0)
1594
1595
1596 def bail (msg):
1597     noise (msg)
1598     noise ("")
1599     usage (err=True)
1600     raise Unreachable
1601
1602
1603 def parse_argv (argv):
1604     global SELF
1605     mode          = PDTCRYPT_DECRYPT
1606     secret        = None
1607     insspec       = None
1608     outsspec      = None
1609     nacl          = None
1610     scrypt_format = PDTCRYPT_SCRYPT_DEFAULT
1611
1612     argvi = iter (argv)
1613     SELF  = os.path.basename (next (argvi))
1614
1615     try:
1616         rawsubcmd  = next (argvi)
1617         subcommand = PDTCRYPT_SUB [rawsubcmd]
1618     except StopIteration:
1619         bail ("ERROR: subcommand required")
1620     except KeyError:
1621         bail ("ERROR: invalid subcommand “%s” specified" % rawsubcmd)
1622
1623     def checked_arg ():
1624         nonlocal argvi
1625         try:
1626             return next (argvi)
1627         except StopIteration:
1628             bail ("ERROR: argument list incomplete")
1629
1630     def checked_secret (t, arg):
1631         nonlocal secret
1632         if secret is None:
1633             secret = (t, arg)
1634         else:
1635             bail ("ERROR: encountered “%s” but secret already given" % arg)
1636
1637     for arg in argvi:
1638         if arg in [ "-h", "--help" ]:
1639             usage ()
1640             raise Unreachable
1641         elif arg in [ "-v", "--verbose", "--wtf" ]:
1642             global PDTCRYPT_VERBOSE
1643             PDTCRYPT_VERBOSE = True
1644         elif arg in [ "-i", "--in", "--source" ]:
1645             insspec = checked_arg ()
1646             if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt from %s" % insspec)
1647         elif arg in [ "-p", "--password" ]:
1648             arg = checked_arg ()
1649             checked_secret (PDTCRYPT_SECRET_PW, arg)
1650             if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypting with password")
1651         else:
1652             if subcommand == PDTCRYPT_SUB_PROCESS:
1653                 if arg in [ "-s", "--strict-ivs" ]:
1654                     global PDTCRYPT_STRICTIVS
1655                     PDTCRYPT_STRICTIVS = True
1656                 elif arg in [ "-o", "--out", "--dest", "--sink" ]:
1657                     outsspec = checked_arg ()
1658                     if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
1659                 elif arg in [ "-f", "--force" ]:
1660                     global PDTCRYPT_OVERWRITE
1661                     PDTCRYPT_OVERWRITE = True
1662                     if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
1663                 elif arg in [ "-S", "--split" ]:
1664                     mode |= PDTCRYPT_SPLIT
1665                     if PDTCRYPT_VERBOSE is True: noise ("PDT: split files")
1666                 elif arg in [ "-D", "--no-decrypt" ]:
1667                     mode &= ~PDTCRYPT_DECRYPT
1668                     if PDTCRYPT_VERBOSE is True: noise ("PDT: not decrypting")
1669                 elif arg in [ "-k", "--key" ]:
1670                     arg = checked_arg ()
1671                     checked_secret (PDTCRYPT_SECRET_KEY, arg)
1672                     if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypting with key")
1673                 else:
1674                     bail ("ERROR: unexpected positional argument “%s”" % arg)
1675             elif subcommand == PDTCRYPT_SUB_SCRYPT:
1676                 if arg in [ "-n", "--nacl", "--salt" ]:
1677                     nacl = checked_arg ()
1678                     if PDTCRYPT_VERBOSE is True: noise ("PDT: salt key with %s" % nacl)
1679                 elif arg in [ "-f", "--format" ]:
1680                     arg = checked_arg ()
1681                     try:
1682                         scrypt_format = PDTCRYPT_SCRYPT_FORMAT [arg]
1683                     except KeyError:
1684                         bail ("ERROR: invalid scrypt output format %s" % arg)
1685                     if PDTCRYPT_VERBOSE is True:
1686                         noise ("PDT: scrypt output format “%s”" % scrypt_format)
1687                 else:
1688                     bail ("ERROR: unexpected positional argument “%s”" % arg)
1689
1690     if secret is None:
1691         if PDTCRYPT_VERBOSE is True:
1692             noise ("ERROR: no password or key specified, trying $PDTCRYPT_PASSWORD")
1693         epw = os.getenv ("PDTCRYPT_PASSWORD")
1694         if epw is not None:
1695             checked_secret (PDTCRYPT_SECRET_PW, epw.strip ())
1696
1697     if secret is None:
1698         if PDTCRYPT_VERBOSE is True:
1699             noise ("ERROR: no password or key specified, trying $PDTCRYPT_KEY")
1700         ek = os.getenv ("PDTCRYPT_KEY")
1701         if ek is not None:
1702             checked_secret (PDTCRYPT_SECRET_KEY, ek.strip ())
1703
1704     if secret is None:
1705         if subcommand == PDTCRYPT_SUB_SCRYPT:
1706             bail ("ERROR: scrypt hash mode requested but no password given")
1707         elif mode & PDTCRYPT_DECRYPT:
1708             bail ("ERROR: encryption requested but no password given")
1709
1710     if subcommand == PDTCRYPT_SUB_SCRYPT:
1711         if secret [0] == PDTCRYPT_SECRET_KEY:
1712             bail ("ERROR: scrypt mode requires a password")
1713         if     insspec is not None and nacl is not None \
1714             or insspec is     None and nacl is     None :
1715                 bail ("ERROR: please supply either an input file or "
1716                       "the salt")
1717
1718     # default to stdout
1719     ins = None
1720     if insspec is not None or subcommand != PDTCRYPT_SUB_SCRYPT:
1721         ins = deptdcrypt_mk_stream (PDTCRYPT_SOURCE, insspec or "-")
1722
1723     if subcommand == PDTCRYPT_SUB_SCRYPT:
1724         return True, partial (mode_scrypt, secret [1].encode (), ins, nacl,
1725                               fmt=scrypt_format)
1726
1727     if mode & PDTCRYPT_SPLIT: # destination must be directory
1728         if outsspec is None or outsspec == "-":
1729             bail ("ERROR: split mode is incompatible with stdout sink")
1730
1731         try:
1732             try:
1733                 os.makedirs (outsspec, 0o700)
1734             except FileExistsError:
1735                 # if it’s a directory with appropriate perms, everything is
1736                 # good; otherwise, below invocation of open(2) will fail
1737                 pass
1738             outs = os.open (outsspec, os.O_DIRECTORY, 0o600)
1739         except FileNotFoundError as exn:
1740             bail ("ERROR: cannot create target directory “%s”" % outsspec)
1741         except NotADirectoryError as exn:
1742             bail ("ERROR: target path “%s” is not a directory" % outsspec)
1743
1744     else:
1745         outs = deptdcrypt_mk_stream (PDTCRYPT_SINK, outsspec or "-")
1746
1747     return True, partial (mode_depdtcrypt, mode, secret, ins, outs)
1748
1749
1750 def main (argv):
1751     ok, runner = parse_argv (argv)
1752
1753     if ok is True: return runner ()
1754
1755     return 1
1756
1757
1758 if __name__ == "__main__":
1759     sys.exit (main (sys.argv))
1760