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