6 ===============================================================================
7 crypto -- Encryption Layer for the Deltatar Backup
8 ===============================================================================
12 - AES-GCM for the symmetric encryption;
17 - NIST Recommendation for Block Cipher Modes of Operation: Galois/Counter
19 http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
22 https://cryptome.org/2014/01/aes-gcm-v1.pdf
24 - Authentication weaknesses in GCM
25 http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/comments/CWC-GCM/Ferguson2.pdf
28 -------------------------------------------------------------------------------
30 Errors fall into roughly three categories:
32 - Cryptographical errors or invalid data.
34 - ``InvalidGCMTag`` (decryption failed on account of an invalid GCM
36 - ``InvalidIVFixedPart`` (IV fixed part of object not found in list),
37 - ``DuplicateIV`` (the IV of an encrypted object already occurred),
38 - ``DecryptionError`` (used in CLI decryption for presenting error
39 conditions to the user).
41 - Incorrect usage of the library.
43 - ``InvalidParameter`` (non-conforming user supplied parameter),
44 - ``InvalidHeader`` (data passed for reading not parsable into header),
45 - ``FormatError`` (cannot handle header or parameter version),
48 - Bad internal state. If one of these is encountered it means that a state
49 was reached that shouldn’t occur during normal processing.
54 Also, ``EndOfFile`` is used as a sentinel to communicate that a stream supplied
55 for reading is exhausted.
57 Initialization Vectors
58 -------------------------------------------------------------------------------
60 Initialization vectors are checked for reuse during the lifetime of a decryptor.
61 The fixed counters for metadata files cannot be reused and attempts to do so
62 will cause a DuplicateIV error. This means the length of objects encrypted with
63 a metadata counter is capped at 63 GB.
65 For ordinary, non-metadata payload, there is an optional mode with strict IV
66 checking that causes a crypto context to fail if an IV encountered or created
67 was already used for decrypting or encrypting, respectively, an earlier object.
68 Note that this mode can trigger false positives when decrypting non-linearly,
69 e. g. when traversing the same object multiple times. Since the crypto context
70 has no notion of a position in a PDT encrypted archive, this condition must be
71 sorted out downstream.
73 When encrypting with more than one Encrypt context special care must be taken
74 to prevent accidental reuse of IVs. The builtin protection against reuse is
75 only effective for objects encrypted with the same Encrypt handle. If multiple
76 Encrypt handles are used to encrypt with the same combination of password and
77 salt, the encryption becomes susceptible to birthday attacks (bound = 2^32 due
78 to the 64-bit random iv). Thus the use of multiple handles is discouraged.
82 -------------------------------------------------------------------------------
84 ``crypto.py`` may be invoked as a script for decrypting, validating, and
85 splitting PDT encrypted files. Consult the usage message for details.
89 Decrypt from stdin using the password ‘foo’: ::
91 $ crypto.py process foo -i - -o - <some-file.tar.gz.pdtcrypt >some-file.tar.gz
93 Output verbose information about the encrypted objects in the archive: ::
95 $ crypto.py process foo -v -i some-file.tar.gz.pdtcrypt -o /dev/null
96 PDT: decrypt from some-file.tar.gz.pdtcrypt
97 PDT: decrypt to /dev/null
98 PDT: source: file some-file.tar.gz.pdtcrypt
99 PDT: sink: file /dev/null
101 PDT: · version = 1 : 0100
102 PDT: · paramversion = 1 : 0100
103 PDT: · nacl : d270 b031 00d1 87e2 c946 610d 7b7f 7e5f
104 PDT: · iv : 02ee 3dd7 a963 1eb1 0100 0000
105 PDT: · ctsize = 591 : 4f02 0000 0000 0000
106 PDT: · tag : 5b2d 6d8b 8f82 4842 12fd 0b10 b6e3 369b
107 PDT: 64 decrypt obj no. 1, 591 B
108 PDT: · [64] 0% done, read block (591 B of 591 B remaining)
109 PDT: · decrypt ciphertext 591 B
110 PDT: · decrypt plaintext 591 B
114 Also, the mode *scrypt* allows deriving encryption keys. To calculate the
115 encryption key from the password ‘foo’ and the salt of the first object in a
116 PDT encrypted file: ::
118 $ crypto.py scrypt foo -i some-file.pdtcrypt
119 {"paramversion": 1, "salt": "Cqzbk48e3peEjzWto8D0yA==", "key": "JH9EkMwaM4x9F5aim5gK/Q=="}
121 The computed 16 byte key is given in hexadecimal notation in the value to
122 ``hash`` and can be fed into Python’s ``binascii.unhexlify()`` to obtain the
123 corresponding binary representation.
125 Note that in Scrypt hashing mode, no data integrity checks are being performed.
126 If the wrong password is given, a wrong key will be derived. Whether the password
127 was indeed correct can only be determined by decrypting. Note that since PDT
128 archives essentially consist of a stream of independent objects, the salt and
129 other parameters may change. Thus a key derived using above method from the
130 first object doesn’t necessarily apply to any of the subsequent objects.
139 from functools import reduce, partial
150 except ImportError as exn:
153 if __name__ == "__main__": ## Work around the import mechanism lest Python’s
154 pwd = os.getcwd() ## preference for local imports causes a cyclical
155 ## import (crypto → pylibscrypt → […] → ./tarfile → crypto).
156 sys.path = [ p for p in sys.path if p.find ("deltatar") < 0 ]
159 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
160 from cryptography.hazmat.backends import default_backend
164 __all__ = [ "hdr_make", "hdr_read", "hdr_fmt", "hdr_fmt_pretty"
166 , "PDTCRYPT_HDR_SIZE", "AES_GCM_IV_CNT_DATA"
167 , "AES_GCM_IV_CNT_INFOFILE", "AES_GCM_IV_CNT_INDEX"
171 ###############################################################################
173 ###############################################################################
175 class EndOfFile (Exception):
179 def __init__ (self, n=None, msg=None):
185 class InvalidParameter (Exception):
186 """Inputs not valid for PDT encryption."""
190 class InvalidHeader (Exception):
191 """Header not valid."""
195 class InvalidGCMTag (Exception):
197 The GCM tag calculated during decryption differs from that in the object
203 class InvalidIVFixedPart (Exception):
205 IV fixed part not in supplied list: either the backup is corrupt or the
206 current object does not belong to it.
211 class IVFixedPartError (Exception):
213 Error creating a unique IV fixed part: repeated calls to system RNG yielded
214 the same sequence of bytes as the last IV used.
219 class InvalidFileCounter (Exception):
221 When encrypting, an attempted reuse of a dedicated counter (info file,
222 index file) was caught.
227 class DuplicateIV (Exception):
229 During encryption, the current IV fixed part is identical to an already
230 existing IV (same prefix and file counter). This indicates tampering or
231 programmer error and cannot be recovered from.
236 class NonConsecutiveIV (Exception):
238 IVs not numbered consecutively. This is a hard error with strict IV
239 checking. Precludes random access to the encrypted objects.
244 class CiphertextTooLong (Exception):
246 An attempt was made to decrypt more data than the ciphertext size declared
247 in the object header.
252 class FormatError (Exception):
253 """Unusable parameters in header."""
257 class DecryptionError (Exception):
258 """Error during decryption with ``crypto.py`` on the command line."""
262 class Unreachable (Exception):
264 Makeshift __builtin_unreachable(); always a programmer error if
270 class InternalError (Exception):
271 """Errors not ascribable to bad user inputs or cryptography."""
275 ###############################################################################
276 ## crypto layer version
277 ###############################################################################
279 ENCRYPTION_PARAMETERS = \
281 { "kdf": ("dummy", 16)
282 , "enc": "passthrough" }
290 , "enc": "aes-gcm" } }
292 # Mode zero is unencrypted and only provided for testing purposes. nless
293 # the encryptor / decryptor are explicitly instructed to do so.
294 MIN_SECURE_PARAMETERS = 1
296 ###############################################################################
298 ###############################################################################
300 PDTCRYPT_HDR_MAGIC = b"PDTCRYPT"
302 PDTCRYPT_HDR_SIZE_MAGIC = 8 # 8
303 PDTCRYPT_HDR_SIZE_VERSION = 2 # 10
304 PDTCRYPT_HDR_SIZE_PARAMVERSION = 2 # 12
305 PDTCRYPT_HDR_SIZE_NACL = 16 # 28
306 PDTCRYPT_HDR_SIZE_IV = 12 # 40
307 PDTCRYPT_HDR_SIZE_CTSIZE = 8 # 48
308 PDTCRYPT_HDR_SIZE_TAG = 16 # 64 GCM auth tag
310 PDTCRYPT_HDR_SIZE = PDTCRYPT_HDR_SIZE_MAGIC + PDTCRYPT_HDR_SIZE_VERSION \
311 + PDTCRYPT_HDR_SIZE_PARAMVERSION + PDTCRYPT_HDR_SIZE_NACL \
312 + PDTCRYPT_HDR_SIZE_IV + PDTCRYPT_HDR_SIZE_CTSIZE \
313 + PDTCRYPT_HDR_SIZE_TAG # = 64
315 # precalculate offsets since Python can’t do constant folding over names
316 HDR_OFF_VERSION = PDTCRYPT_HDR_SIZE_MAGIC
317 HDR_OFF_PARAMVERSION = HDR_OFF_VERSION + PDTCRYPT_HDR_SIZE_VERSION
318 HDR_OFF_NACL = HDR_OFF_PARAMVERSION + PDTCRYPT_HDR_SIZE_PARAMVERSION
319 HDR_OFF_IV = HDR_OFF_NACL + PDTCRYPT_HDR_SIZE_NACL
320 HDR_OFF_CTSIZE = HDR_OFF_IV + PDTCRYPT_HDR_SIZE_IV
321 HDR_OFF_TAG = HDR_OFF_CTSIZE + PDTCRYPT_HDR_SIZE_CTSIZE
325 FMT_I2N_IV = "<8sL" # 8 random bytes ‖ 32 bit counter
326 FMT_I2N_HDR = ("<" # host byte order
330 "16s" # sodium chloride
336 AES_KEY_SIZE = 16 # b"0123456789abcdef"
337 AES_KEY_SIZE_B64 = 24 # b'MDEyMzQ1Njc4OWFiY2RlZg=='
339 AES_GCM_MAX_SIZE = (1 << 36) - (1 << 5) # 2^39 - 2^8 b ≅ 64 GB.
340 # Source: NIST SP 800-38D section 5.2.1.1
341 # https://crypto.stackexchange.com/questions/31793/plain-text-size-limits-for-aes-gcm-mode-just-64gb
343 PDTCRYPT_MAX_OBJ_SIZE_DEFAULT = 63 * (1 << 30) # 63 GB
344 PDTCRYPT_MAX_OBJ_SIZE = PDTCRYPT_MAX_OBJ_SIZE_DEFAULT
346 # index and info files are written on-the fly while encrypting so their
347 # counters must be available in advance
348 AES_GCM_IV_CNT_INFOFILE = 1 # constant
349 AES_GCM_IV_CNT_INDEX = AES_GCM_IV_CNT_INFOFILE + 1
350 AES_GCM_IV_CNT_DATA = AES_GCM_IV_CNT_INDEX + 1 # also for multivolume
351 AES_GCM_IV_CNT_MAX_DEFAULT = 0xffFFffFF
352 AES_GCM_IV_CNT_MAX = AES_GCM_IV_CNT_MAX_DEFAULT
354 # IV structure and generation
355 PDTCRYPT_IV_GEN_MAX_RETRIES = 10 # ×
356 PDTCRYPT_IV_FIXEDPART_SIZE = 8 # B
357 PDTCRYPT_IV_COUNTER_SIZE = 4 # B
359 # secret type: PW of string | KEY of char [16]
360 PDTCRYPT_SECRET_PW = 0
361 PDTCRYPT_SECRET_KEY = 1
363 ###############################################################################
365 ###############################################################################
371 # , paramversion : u16
377 # fn hdr_read (f : handle) -> hdrinfo;
378 # fn hdr_make (f : handle, h : hdrinfo) -> IOResult<usize>;
379 # fn hdr_fmt (h : hdrinfo) -> String;
384 Read bytes as header structure.
386 If the input could not be interpreted as a header, fail with
391 mag, version, paramversion, nacl, iv, ctsize, tag = \
392 struct.unpack (FMT_I2N_HDR, data)
393 except Exception as exn:
394 raise InvalidHeader ("error unpacking header from [%r]: %s"
395 % (binascii.hexlify (data), str (exn)))
397 if mag != PDTCRYPT_HDR_MAGIC:
398 raise InvalidHeader ("bad magic in header: expected [%s], got [%s]"
399 % (PDTCRYPT_HDR_MAGIC, mag))
402 { "version" : version
403 , "paramversion" : paramversion
411 def hdr_read_stream (instr):
413 Read header from stream at the current position.
415 Fail with ``InvalidHeader`` if insufficient bytes were read from the
416 stream, or if the content could not be interpreted as a header.
418 data = instr.read(PDTCRYPT_HDR_SIZE)
422 elif ldata != PDTCRYPT_HDR_SIZE:
423 raise InvalidHeader ("hdr_read_stream: expected %d B, received %d B"
424 % (PDTCRYPT_HDR_SIZE, ldata))
425 return hdr_read (data)
428 def hdr_from_params (version, paramversion, nacl, iv, ctsize, tag):
430 Assemble the necessary values into a PDTCRYPT header.
432 :type version: int to fit uint16_t
433 :type paramversion: int to fit uint16_t
434 :type nacl: bytes to fit uint8_t[16]
435 :type iv: bytes to fit uint8_t[12]
436 :type size: int to fit uint64_t
437 :type tag: bytes to fit uint8_t[16]
439 buf = bytearray (PDTCRYPT_HDR_SIZE)
440 bufv = memoryview (buf)
443 struct.pack_into (FMT_I2N_HDR, bufv, 0,
445 version, paramversion, nacl, iv, ctsize, tag)
446 except Exception as exn:
447 return False, "error assembling header: %s" % str (exn)
449 return True, bytes (buf)
452 def hdr_make_dummy (s):
454 Create a header sized block of bytes initialized to a value derived from a
455 string. Used to verify we’ve jumped back correctly to the actual position
456 of the object header.
458 c = reduce (lambda a, c: a + ord(c), s, 0) % 0xFF
459 return bytes (bytearray (struct.pack ("B", c)) * PDTCRYPT_HDR_SIZE)
464 Assemble a header from the given header structure.
466 return hdr_from_params (version=hdr.get("version"),
467 paramversion=hdr.get("paramversion"),
468 nacl=hdr.get("nacl"), iv=hdr.get("iv"),
469 ctsize=hdr.get("ctsize"), tag=hdr.get("tag"))
472 HDR_FMT = "I2n_header { version: %d, paramversion: %d, nacl: %s[%d]," \
473 " iv: %s[%d], ctsize: %d, tag: %s[%d] }"
476 """Format a header structure into readable output."""
477 return HDR_FMT % (h["version"], h["paramversion"],
478 binascii.hexlify (h["nacl"]), len(h["nacl"]),
479 binascii.hexlify (h["iv"]), len(h["iv"]),
481 binascii.hexlify (h["tag"]), len(h["tag"]))
484 def hex_spaced_of_bytes (b):
485 """Format bytes object, hexdump style."""
486 return " ".join ([ "%.2x%.2x" % (c1, c2)
487 for c1, c2 in zip (b[0::2], b[1::2]) ]) \
488 + (len (b) | 1 == len (b) and " %.2x" % b[-1] or "") # odd lengths
491 def hdr_iv_counter (h):
492 """Extract the variable part of the IV of the given header."""
493 _fixed, cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
497 def hdr_iv_fixed (h):
498 """Extract the fixed part of the IV of the given header."""
499 fixed, _cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
503 hdr_dump = hex_spaced_of_bytes
507 """version = %-4d : %s
508 paramversion = %-4d : %s
515 def hdr_fmt_pretty (h):
517 Format header structure into multi-line representation of its contents and
518 their raw representation. (Omit the implicit “PDTCRYPT” magic bytes that
519 precede every header.)
521 return HDR_FMT_PRETTY \
523 hex_spaced_of_bytes (struct.pack (FMT_UINT16_LE, h["version"])),
525 hex_spaced_of_bytes (struct.pack (FMT_UINT16_LE, h["paramversion"])),
526 hex_spaced_of_bytes (h["nacl"]),
527 hex_spaced_of_bytes (h["iv"]),
529 hex_spaced_of_bytes (struct.pack (FMT_UINT64_LE, h["ctsize"])),
530 hex_spaced_of_bytes (h["tag"]))
532 IV_FMT = "((f %s) (c %d))"
535 """Format the two components of an IV in a readable fashion."""
536 fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
537 return IV_FMT % (binascii.hexlify (fixed), cnt)
540 ###############################################################################
542 ###############################################################################
544 class Location (object):
548 def restore_loc_fmt (loc):
550 % (loc.n, loc.offset)
552 def locate_hdr_candidates (fd):
554 Walk over instances of the magic string in the payload, collecting their
555 positions. If the offset of the first found instance is not zero, the file
556 begins with leading garbage. Used by desaster recovery.
558 :return: The list of offsets in the file.
562 mm = mmap.mmap(fd, 0, mmap.MAP_SHARED, mmap.PROT_READ)
565 pos = mm.find (PDTCRYPT_HDR_MAGIC, pos)
574 HDR_CAND_GOOD = 0 # header marks begin of valid object
575 HDR_CAND_FISHY = 1 # inconclusive (tag mismatch, obj overlap etc.)
576 HDR_CAND_JUNK = 2 # not a header / object unreadable
579 { HDR_CAND_GOOD : "valid"
580 , HDR_CAND_FISHY : "fishy"
581 , HDR_CAND_JUNK : "junk"
585 def verdict_fmt (vdt):
586 return HDR_VERDICT_NAME [vdt]
589 def inspect_hdr (fd, off):
591 Attempt to parse a header in *fd* at position *off*.
593 Returns a verdict about the quality of that header plus the parsed header
597 _ = os.lseek (fd, off, os.SEEK_SET)
599 if os.lseek (fd, 0, os.SEEK_CUR) != off:
600 if PDTCRYPT_VERBOSE is True:
601 noise ("PDT: %d → dismissed (lseek() past EOF)" % off)
602 return HDR_CAND_JUNK, None
604 raw = os.read (fd, PDTCRYPT_HDR_SIZE)
605 if len (raw) != PDTCRYPT_HDR_SIZE:
606 if PDTCRYPT_VERBOSE is True:
607 noise ("PDT: %d → dismissed (EOF inside header)" % off)
608 return HDR_CAND_JUNK, None
612 except InvalidHeader as exn:
613 if PDTCRYPT_VERBOSE is True:
614 noise ("PDT: %d → dismissed (invalid: [%s])" % (off, str (exn)))
615 return HDR_CAND_JUNK, None
617 obj0 = off + PDTCRYPT_HDR_SIZE
618 objX = obj0 + hdr ["ctsize"]
620 eof = os.lseek (fd, 0, os.SEEK_END)
622 if PDTCRYPT_VERBOSE is True:
623 noise ("PDT: %d → EOF inside object (%d≤%d≤%d); adjusting size to "
624 "%d" % (off, obj0, eof, objX, (eof - obj0)))
625 # try reading up to the end
626 hdr ["ctsize"] = eof - obj0
627 return HDR_CAND_FISHY, hdr
629 return HDR_CAND_GOOD, hdr
632 def try_decrypt (ifd, off, hdr, secret, ofd=-1):
634 Attempt to decrypt the object in the (seekable) descriptor *ifd* starting
635 at *off* using the metadata in *hdr* and *secret*. An output fd can be
636 specified with *ofd*; if it is *-1* – the default –, the decrypted payload
639 Always creates a fresh decryptor, so validation steps across objects don’t
642 Errors during GCM tag validation are ignored. Used by desaster recovery.
644 ctleft = hdr ["ctsize"]
648 if ks == PDTCRYPT_SECRET_PW:
649 decr = Decrypt (password=secret [1])
650 elif ks == PDTCRYPT_SECRET_KEY:
652 decr = Decrypt (key=key)
659 os.lseek (ifd, pos, os.SEEK_SET)
662 cnksiz = min (ctleft, PDTCRYPT_BLOCKSIZE)
663 cnk = os.read (ifd, cnksiz)
666 pt = decr.process (cnk)
671 except InvalidGCMTag:
672 noise ("PDT: GCM tag mismatch for object %d–%d"
673 % (off, off + hdr ["ctsize"]))
674 if len (pt) > 0 and ofd != -1:
677 except Exception as exn:
678 noise ("PDT: error decrypting object %d–%d@%d, %d B remaining [%s]"
679 % (off, off + hdr ["ctsize"], pos, ctleft, exn))
685 def readable_objects_offsets (ifd, secret, cands):
687 From a list of candidates, locate the ones that mark the start of actual
688 readable PDTCRYPT objects.
692 for i, cand in enumerate (cands):
693 vdt, hdr = inspect_hdr (ifd, cand)
694 if vdt == HDR_CAND_JUNK:
695 pass # ignore unreadable ones
696 elif vdt in [HDR_CAND_GOOD, HDR_CAND_FISHY]:
697 ctsize = hdr ["ctsize"]
698 off0 = cand + PDTCRYPT_HDR_SIZE
699 ok = try_decrypt (ifd, off0, hdr, secret) == ctsize
701 good.append ((cand, off0 + ctsize))
703 overlap = find_overlaps (good)
705 return [ g [0] for g in good ]
708 def reconstruct_offsets (fname, secret):
709 ifd = os.open (fname, os.O_RDONLY)
712 cands = locate_hdr_candidates (ifd)
713 return readable_objects_offsets (ifd, secret, cands)
718 ###############################################################################
720 ###############################################################################
722 def make_secret (password=None, key=None):
724 Safely create a “secret” value that consists either of a key or a password.
725 Inputs are validated: the password is accepted as (UTF-8 encoded) bytes or
726 string; for the key only a bytes object of the proper size or a base64
727 encoded string thereof is accepted.
729 If both are provided, the key is preferred over the password; no checks are
730 performed whether the key is derived from the password.
732 :returns: secret value if inputs were acceptable | None otherwise.
735 if isinstance (key, str) is True:
736 key = key.encode ("utf-8")
737 if isinstance (key, bytes) is True:
738 if len (key) == AES_KEY_SIZE:
739 return (PDTCRYPT_SECRET_KEY, key)
740 if len (key) == AES_KEY_SIZE * 2:
742 key = binascii.unhexlify (key)
743 return (PDTCRYPT_SECRET_KEY, key)
744 except binascii.Error: # garbage in string
746 if len (key) == AES_KEY_SIZE_B64:
748 key = base64.b64decode (key)
749 # the base64 processor is very tolerant and allows for
750 # arbitrary trailing and leading data thus the data obtained
751 # must be checked for the proper length
752 if len (key) == AES_KEY_SIZE:
753 return (PDTCRYPT_SECRET_KEY, key)
754 except binascii.Error: # “incorrect padding”
756 elif password is not None:
757 if isinstance (password, str) is True:
758 return (PDTCRYPT_SECRET_PW, password)
759 elif isinstance (password, bytes) is True:
761 password = password.decode ("utf-8")
762 return (PDTCRYPT_SECRET_PW, password)
763 except UnicodeDecodeError:
769 ###############################################################################
770 ## passthrough / null encryption
771 ###############################################################################
773 class PassthroughCipher (object):
775 tag = struct.pack ("<QQ", 0, 0)
777 def __init__ (self) : pass
779 def update (self, b) : return b
781 def finalize (self) : return b""
783 def finalize_with_tag (self, _) : return b""
785 ###############################################################################
786 ## convenience wrapper
787 ###############################################################################
790 def kdf_dummy (klen, password, _nacl):
792 Fake KDF for testing purposes that is called when parameter version zero is
795 q, r = divmod (klen, len (password))
796 if isinstance (password, bytes) is False:
797 password = password.encode ()
798 return password * q + password [:r], b""
801 SCRYPT_KEY_MEMO = { } # static because needed for both the info file and the archive
804 def kdf_scrypt (params, password, nacl):
806 Wrapper for the Scrypt KDF, corresponds to parameter version one. The
807 computation result is memoized based on the inputs to facilitate spawning
808 multiple encryption contexts.
813 dkLen = params["dkLen"]
816 nacl = os.urandom (params["NaCl_LEN"])
818 key_parms = (password, nacl, N, r, p, dkLen)
819 global SCRYPT_KEY_MEMO
820 if key_parms not in SCRYPT_KEY_MEMO:
821 SCRYPT_KEY_MEMO [key_parms] = \
822 pylibscrypt.scrypt (password, nacl, N, r, p, dkLen)
823 return SCRYPT_KEY_MEMO [key_parms], nacl
826 def kdf_by_version (paramversion=None, defs=None):
828 Pick the KDF handler corresponding to the parameter version or the
831 :rtype: function (password : str, nacl : str) -> str
833 if paramversion is not None:
834 defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
836 raise InvalidParameter ("no encryption parameters for version %r"
838 (kdf, params) = defs["kdf"]
840 if kdf == "scrypt" : fn = kdf_scrypt
841 elif kdf == "dummy" : fn = kdf_dummy
843 raise ValueError ("key derivation method %r unknown" % kdf)
844 return partial (fn, params)
847 ###############################################################################
849 ###############################################################################
851 def scrypt_hashsource (pw, ins):
853 Calculate the SCRYPT hash from the password and the information contained
854 in the first header found in ``ins``.
856 This does not validate whether the first object is encrypted correctly.
858 if isinstance (pw, str) is True:
860 elif isinstance (pw, bytes) is False:
861 raise InvalidParameter ("password must be a string, not %s"
863 if isinstance (ins, io.BufferedReader) is False and \
864 isinstance (ins, io.FileIO) is False:
865 raise InvalidParameter ("file to hash must be opened in “binary” mode")
868 hdr = hdr_read_stream (ins)
869 except EndOfFile as exn:
870 noise ("PDT: malformed input: end of file reading first object header")
875 pver = hdr ["paramversion"]
876 if PDTCRYPT_VERBOSE is True:
877 noise ("PDT: salt of first object : %s" % binascii.hexlify (nacl))
878 noise ("PDT: parameter version of archive : %d" % pver)
881 defs = ENCRYPTION_PARAMETERS.get(pver, None)
882 kdfname, params = defs ["kdf"]
883 if kdfname != "scrypt":
884 noise ("PDT: input is not an SCRYPT archive")
887 kdf = kdf_by_version (None, defs)
888 except ValueError as exn:
889 noise ("PDT: object has unknown parameter version %d" % pver)
891 hsh, _void = kdf (pw, nacl)
893 return hsh, nacl, hdr ["version"], pver
896 def scrypt_hashfile (pw, fname):
898 Calculate the SCRYPT hash from the password and the information contained
899 in the first header found in the given file. The header is read only at
902 with deptdcrypt_mk_stream (PDTCRYPT_SOURCE, fname or "-") as ins:
903 hsh, _void, _void, _void = scrypt_hashsource (pw, ins)
907 ###############################################################################
909 ###############################################################################
911 class Crypto (object):
913 Encryption context to remain alive throughout an entire tarfile pass.
918 cnt = None # file counter (uint32_t != 0)
919 iv = None # current IV
920 fixed = None # accu for 64 bit fixed parts of IV
921 used_ivs = None # tracks IVs
922 strict_ivs = False # if True, panic on duplicate or non-consecutive object IV
925 insecure = False # allow plaintext parameters
932 info_counter_used = False
933 index_counter_used = False
935 def __init__ (self, *al, **akv):
936 self.used_ivs = set ()
937 self.set_parameters (*al, **akv)
940 def next_fixed (self):
945 def set_object_counter (self, cnt=None):
947 Safely set the internal counter of encrypted objects. Numerous
950 The same counter may not be reused in combination with one IV fixed
951 part. This is validated elsewhere in the IV handling.
953 Counter zero is invalid. The first two counters are reserved for
954 metadata. The implementation does not allow for splitting metadata
955 files over multiple encrypted objects. (This would be possible by
956 assigning new fixed parts.) Thus in a Deltatar backup there is at most
957 one object with a counter value of one and two. On creation of a
958 context, the initial counter may be chosen. The globals
959 ``AES_GCM_IV_CNT_INFOFILE`` and ``AES_GCM_IV_CNT_INDEX`` can be used to
960 request one of the reserved values. If one of these values has been
961 used, any further attempt of setting the counter to that value will
962 be rejected with an ``InvalidFileCounter`` exception.
964 Out of bounds values (i. e. below one and more than the maximum of 2³²)
965 cause an ``InvalidParameter`` exception to be thrown.
968 self.cnt = AES_GCM_IV_CNT_DATA
970 if cnt == 0 or cnt > AES_GCM_IV_CNT_MAX + 1:
971 raise InvalidParameter ("invalid counter value %d requested: "
972 "acceptable values are from 1 to %d"
973 % (cnt, AES_GCM_IV_CNT_MAX))
974 if cnt == AES_GCM_IV_CNT_INFOFILE:
975 if self.info_counter_used is True:
976 raise InvalidFileCounter ("attempted to reuse info file "
977 "counter %d: must be unique" % cnt)
978 self.info_counter_used = True
979 elif cnt == AES_GCM_IV_CNT_INDEX:
980 if self.index_counter_used is True:
981 raise InvalidFileCounter ("attempted to reuse index file "
982 "counter %d: must be unique" % cnt)
983 self.index_counter_used = True
984 if cnt <= AES_GCM_IV_CNT_MAX:
987 # cnt == AES_GCM_IV_CNT_MAX + 1 → wrap
988 self.cnt = AES_GCM_IV_CNT_DATA
992 def set_parameters (self, password=None, key=None, paramversion=None,
993 nacl=None, counter=None, strict_ivs=False,
996 Configure the internal state of a crypto context. Not intended for
999 A parameter version indicating passthrough (plaintext) mode is rejected
1000 with an ``InvalidParameter`` unless ``insecure`` is set.
1003 self.set_object_counter (counter)
1004 self.strict_ivs = strict_ivs
1006 self.insecure = insecure
1008 if paramversion is not None:
1009 if self.insecure is False \
1010 and paramversion < MIN_SECURE_PARAMETERS:
1011 raise InvalidParameter \
1012 ("set_parameters: requested parameter version %d but "
1013 "plaintext encryption disallowed in secure context!"
1015 self.paramversion = paramversion
1018 self.key, self.nacl = key, nacl
1021 if password is not None:
1022 if isinstance (password, bytes) is False:
1023 password = str.encode (password)
1024 self.password = password
1025 if paramversion is None and nacl is None:
1026 # postpone key setup until first header is available
1028 kdf = kdf_by_version (paramversion)
1030 self.key, self.nacl = kdf (password, nacl)
1033 def process (self, buf):
1035 Encrypt / decrypt a buffer. Invokes the ``.update()`` method on the
1036 wrapped encryptor or decryptor, respectively.
1038 The Cryptography exception ``AlreadyFinalized`` is translated to an
1039 ``InternalError`` at this point. It may occur in sound code when the GC
1040 closes an encrypting stream after an error. Everywhere else it must be
1043 if self.enc is None:
1044 raise RuntimeError ("process: context not initialized")
1045 self.stats ["in"] += len (buf)
1047 out = self.enc.update (buf)
1048 except cryptography.exceptions.AlreadyFinalized as exn:
1049 raise InternalError (exn)
1050 self.stats ["out"] += len (out)
1054 def next (self, password, paramversion, nacl):
1056 Prepare for encrypting another object: Reset the data counters and
1057 change the configuration in case one of the variable parameters differs
1058 from the last object.
1062 self.stats ["obj"] += 1
1064 if ( self.paramversion != paramversion
1065 or self.password != password
1066 or self.nacl != nacl):
1067 self.set_parameters (password=password, paramversion=paramversion,
1068 nacl=nacl, strict_ivs=self.strict_ivs,
1069 insecure=self.insecure)
1072 def counters (self):
1074 Access the data counters.
1076 return self.stats ["obj"], self.stats ["in"], self.stats ["out"]
1081 Clear the current context regardless of its finalization state. The
1082 next operation must be ``.next()``.
1087 def get_used_ivs (self):
1089 Get the set of IVs that were used so far during the lifetime of
1090 this context. Useful to check for IV reuse if multiple encryption
1091 contexts were used independently.
1093 return self.used_ivs
1096 def reset_last_iv (self):
1098 Implemented only for decryptor; no-op otherwise.
1103 class Encrypt (Crypto):
1109 def __init__ (self, version, paramversion, password=None, key=None, nacl=None,
1110 counter=AES_GCM_IV_CNT_DATA, strict_ivs=False, insecure=False):
1112 The ctor will throw immediately if one of the parameters does not conform
1113 to our expectations.
1115 :type version: int to fit uint16_t
1116 :type paramversion: int to fit uint16_t
1117 :param password: mutually exclusive with ``key``
1118 :type password: bytes
1119 :param key: mutually exclusive with ``password``
1122 :type counter: initial object counter the values
1123 ``AES_GCM_IV_CNT_INFOFILE`` and
1124 ``AES_GCM_IV_CNT_INDEX`` are unique in each backup set
1125 and cannot be reused even with different fixed parts.
1126 :type strict_ivs: bool
1127 :param strict_ivs: Enable paranoid tracking of IVs.
1128 :type insecure: bool
1129 :param insecure: whether to permit passthrough mode
1131 *Security considerations*: The ``class Encrypt`` handle guarantees that
1132 all random parts (first eight bytes) of the IVs used for encrypting
1133 objects are unique. This guarantee does *not* apply across handles if
1134 multiple handles are used with the same combination of password and
1135 salt. Thus, use of multiple handles with the same combination of password
1136 and salt is subject to birthday attacks with a bound of 2^32. To avoid
1137 collisions, the application should keep the number of handles as low
1138 as possible and check for reuse by comparing the set of IVs used of all
1139 handles that were created (accessible using the ``get_used_ivs`` method).
1141 if password is None and key is None \
1142 or password is not None and key is not None :
1143 raise InvalidParameter ("__init__: need either key or password")
1146 if isinstance (key, bytes) is False:
1147 raise InvalidParameter ("__init__: key must be provided as "
1148 "bytes, not %s" % type (key))
1150 raise InvalidParameter ("__init__: salt must be provided along "
1151 "with encryption key")
1152 else: # password, no key
1153 if isinstance (password, str) is False:
1154 raise InvalidParameter ("__init__: password must be a string, not %s"
1156 if len (password) == 0:
1157 raise InvalidParameter ("__init__: supplied empty password but not "
1158 "permitted for PDT encrypted files")
1160 if isinstance (version, int) is False:
1161 raise InvalidParameter ("__init__: version number must be an "
1162 "integer, not %s" % type (version))
1164 raise InvalidParameter ("__init__: version number must be a "
1165 "nonnegative integer, not %d" % version)
1167 if isinstance (paramversion, int) is False:
1168 raise InvalidParameter ("__init__: crypto parameter version number "
1169 "must be an integer, not %s"
1170 % type (paramversion))
1171 if paramversion < 0:
1172 raise InvalidParameter ("__init__: crypto parameter version number "
1173 "must be a nonnegative integer, not %d"
1176 if nacl is not None:
1177 if isinstance (nacl, bytes) is False:
1178 raise InvalidParameter ("__init__: salt given, but of type %s "
1179 "instead of bytes" % type (nacl))
1180 # salt length would depend on the actual encryption so it can’t be
1181 # validated at this point
1183 self.version = version
1184 self.paramenc = ENCRYPTION_PARAMETERS.get (paramversion) ["enc"]
1186 super().__init__ (password, key, paramversion, nacl, counter=counter,
1187 strict_ivs=strict_ivs, insecure=insecure)
1190 def next_fixed (self, retries=PDTCRYPT_IV_GEN_MAX_RETRIES):
1192 Generate the next IV fixed part by reading eight bytes from
1193 ``/dev/urandom``. The buffer so obtained is tested against the fixed
1194 parts used so far to prevent accidental reuse of IVs. After a
1195 configurable number of attempts to create a unique fixed part, it will
1196 refuse to continue with an ``IVFixedPartError``. This is unlikely to
1197 ever happen on a normal system but may detect an issue with the random
1200 The list of fixed parts that were used by the context at hand can be
1201 accessed through the ``.fixed`` list. Its last element is the fixed
1202 part currently in use.
1206 fp = os.urandom (PDTCRYPT_IV_FIXEDPART_SIZE)
1207 if fp not in self.fixed:
1208 self.fixed.append (fp)
1211 raise IVFixedPartError ("error obtaining a unique IV fixed part from "
1212 "/dev/urandom; giving up after %d tries" % i)
1217 Construct a 12-bytes IV from the current fixed part and the object
1220 return struct.pack(FMT_I2N_IV, self.fixed [-1], self.cnt)
1223 def next (self, filename=None, counter=None):
1225 Prepare for encrypting the next incoming object. Update the counter
1226 and put together the IV, possibly changing prefixes. Then create the
1229 The argument ``counter`` can be used to specify a file counter for this
1230 object. Unless it is one of the reserved values, the counter of
1231 subsequent objects will be computed from this one.
1233 If this is the first object in a series, ``filename`` is required,
1234 otherwise it is reused if not present. The value is used to derive a
1235 header sized placeholder to use until after encryption when all the
1236 inputs to construct the final header are available. This is then
1237 matched in ``.done()`` against the value found at the position of the
1238 header. The motivation for this extra check is primarily to assist
1239 format debugging: It makes stray headers easy to spot in malformed
1242 if filename is None:
1243 if self.lastinfo is None:
1244 raise InvalidParameter ("next: filename is mandatory for "
1246 filename, _dummy = self.lastinfo
1248 if isinstance (filename, str) is False:
1249 raise InvalidParameter ("next: filename must be a string, no %s"
1251 if counter is not None:
1252 if isinstance (counter, int) is False:
1253 raise InvalidParameter ("next: the supplied counter is of "
1254 "invalid type %s; please pass an "
1255 "integer instead" % type (counter))
1256 self.set_object_counter (counter)
1258 self.iv = self.iv_make ()
1259 if self.paramenc == "aes-gcm":
1261 ( algorithms.AES (self.key)
1262 , modes.GCM (self.iv)
1263 , backend = default_backend ()) \
1265 elif self.paramenc == "passthrough":
1266 self.enc = PassthroughCipher ()
1268 raise InvalidParameter ("next: parameter version %d not known"
1269 % self.paramversion)
1270 hdrdum = hdr_make_dummy (filename)
1271 self.lastinfo = (filename, hdrdum)
1273 self.check_duplicate_iv (self.iv)
1275 super().next (self.password, self.paramversion, self.nacl)
1277 self.set_object_counter (self.cnt + 1)
1281 def check_duplicate_iv (self, iv):
1283 Add an IV (the 12 byte representation as in the header) to the list. With
1284 strict checking enabled, this will throw a ``DuplicateIV``. Depending on
1285 the context, this may indicate a serious error (IV reuse).
1287 IVs are only tracked in strict_ivs mode.
1289 if self.strict_ivs is False:
1292 if iv in self.used_ivs:
1293 raise DuplicateIV ("iv %s was reused" % iv_fmt (iv))
1294 # vi has not been used before; add to collection
1295 self.used_ivs.add (iv)
1298 def done (self, cmpdata):
1300 Complete encryption of an object. After this has been called, attempts
1301 of encrypting further data will cause an error until ``.next()`` is
1304 Returns a 64 bytes buffer containing the object header including all
1305 values including the “late” ones e. g. the ciphertext size and the
1308 if isinstance (cmpdata, bytes) is False:
1309 raise InvalidParameter ("done: comparison input expected as bytes, "
1310 "not %s" % type (cmpdata))
1311 if self.lastinfo is None:
1312 raise RuntimeError ("done: encryption context not initialized")
1313 filename, hdrdum = self.lastinfo
1314 if cmpdata != hdrdum:
1315 raise RuntimeError ("done: bad sync of header for object %d: "
1316 "preliminary data does not match; this likely "
1317 "indicates a wrongly repositioned stream"
1319 data = self.enc.finalize ()
1320 self.stats ["out"] += len (data)
1321 self.ctsize += len (data)
1322 ok, hdr = hdr_from_params (self.version, self.paramversion, self.nacl,
1323 self.iv, self.ctsize, self.enc.tag)
1325 raise InternalError ("error constructing header: %r" % hdr)
1326 return data, hdr, self.fixed
1329 def process (self, buf):
1331 Encrypt a chunk of plaintext with the active encryptor. Returns the
1332 size of the input consumed. This **must** be checked downstream. If the
1333 maximum possible object size has been reached, the current context must
1334 be finalized and a new one established before any further data can be
1335 encrypted. The second argument is the remainder of the plaintext that
1336 was not encrypted for the caller to use immediately after the new
1339 if isinstance (buf, bytes) is False:
1340 raise InvalidParameter ("process: expected byte buffer, not %s"
1343 newptsize = self.ptsize + bsize
1344 diff = newptsize - PDTCRYPT_MAX_OBJ_SIZE
1347 newptsize = PDTCRYPT_MAX_OBJ_SIZE
1348 self.ptsize = newptsize
1349 data = super().process (buf [:bsize])
1350 self.ctsize += len (data)
1354 class Decrypt (Crypto):
1356 tag = None # GCM tag, part of header
1357 last_iv = None # check consecutive ivs in strict mode
1360 def __init__ (self, password=None, key=None, counter=None, fixedparts=None,
1361 strict_ivs=True, insecure=False):
1363 Sanitizing ctor for the decryption context. ``fixedparts`` specifies a
1364 list of IV fixed parts accepted during decryption. If a fixed part is
1365 encountered that is not in the list, decryption will fail.
1367 :param password: mutually exclusive with ``key``
1368 :type password: bytes
1369 :param key: mutually exclusive with ``password``
1371 :type counter: initial object counter the values
1372 ``AES_GCM_IV_CNT_INFOFILE`` and
1373 ``AES_GCM_IV_CNT_INDEX`` are unique in each backup set
1374 and cannot be reused even with different fixed parts.
1375 :type fixedparts: bytes list
1376 :type strict_ivs: bool
1377 :param strict_ivs: fail if IVs of decrypted objects are not linearly
1379 :type insecure: bool
1380 :param insecure: whether to process objects encrypted in
1381 passthrough mode (*``paramversion`` < 1*)
1383 *Security considerations*: The ``strict_ivs`` setting protects against
1384 ciphertext reordering and injection attacks. For this to work it relies
1385 on a property of how the object counters are created during encryption.
1386 If multiple ``Encrypt`` handles have been used during encryption, this
1387 is property is unlikely to apply as it would require manual management
1388 of counters across Encrypt handles. In these cases it may thus be
1389 necessary to disable the ```strict_ivs`` protection.
1391 if password is None and key is None \
1392 or password is not None and key is not None :
1393 raise InvalidParameter ("__init__: need either key or password")
1396 if isinstance (key, bytes) is False:
1397 raise InvalidParameter ("__init__: key must be provided as "
1398 "bytes, not %s" % type (key))
1399 else: # password, no key
1400 if isinstance (password, str) is False:
1401 raise InvalidParameter ("__init__: password must be a string, not %s"
1403 if len (password) == 0:
1404 raise InvalidParameter ("__init__: supplied empty password but not "
1405 "permitted for PDT encrypted files")
1407 if fixedparts is not None:
1408 if isinstance (fixedparts, list) is False:
1409 raise InvalidParameter ("__init__: IV fixed parts must be "
1410 "supplied as list, not %s"
1411 % type (fixedparts))
1412 self.fixed = fixedparts
1415 super().__init__ (password=password, key=key, counter=counter,
1416 strict_ivs=strict_ivs, insecure=insecure)
1419 def valid_fixed_part (self, iv):
1421 Check if a fixed part was already seen.
1423 # check if fixed part is known
1424 fixed, _cnt = struct.unpack (FMT_I2N_IV, iv)
1425 i = bisect.bisect_left (self.fixed, fixed)
1426 return i != len (self.fixed) and self.fixed [i] == fixed
1429 def reset_last_iv (self):
1431 Force a new IV sequence start. The last IV counter will be set from the
1432 next IV encountered and the check for consecutive IVs will be suppressed.
1434 The intended use is backup volume boundaries or handling batches of
1435 objects encrypted with ``Encrypt`` handles initialized with different
1436 initial counter values.
1440 def check_consecutive_iv (self, iv):
1442 Check whether the counter part of the given IV is indeed the successor
1443 of the currently present counter. This should always be the case for
1444 the objects in a well formed PDT archive but should not be enforced
1445 when decrypting out-of-order.
1447 fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
1448 if self.strict_ivs is True \
1449 and self.last_iv is not None \
1450 and self.last_iv [0] == fixed \
1451 and self.last_iv [1] + 1 != cnt:
1452 raise NonConsecutiveIV ("iv %s counter not successor of "
1453 "last object (expected %d, found %d)"
1454 % (iv_fmt (iv), self.last_iv [1] + 1, cnt))
1455 self.last_iv = (fixed, cnt)
1458 def next (self, hdr):
1460 Start decrypting the next object. The PDTCRYPT header for the object
1461 can be given either as already parsed object or as bytes.
1463 if isinstance (hdr, bytes) is True:
1464 hdr = hdr_read (hdr)
1465 elif isinstance (hdr, dict) is False:
1466 # this won’t catch malformed specs though
1467 raise InvalidParameter ("next: wrong type of parameter hdr: "
1468 "expected bytes or spec, got %s"
1471 paramversion = hdr ["paramversion"]
1475 ctsize = hdr ["ctsize"]
1477 raise InvalidHeader ("next: not a header %r" % hdr)
1479 if ctsize > PDTCRYPT_MAX_OBJ_SIZE:
1480 raise InvalidHeader ("next: ciphertext size %d exceeds maximum "
1482 % (ctsize, PDTCRYPT_MAX_OBJ_SIZE))
1484 self.hdr_ctsize = ctsize
1486 super().next (self.password, paramversion, nacl)
1487 if self.fixed is not None and self.valid_fixed_part (iv) is False:
1488 raise InvalidIVFixedPart ("iv %s has invalid fixed part"
1491 self.check_consecutive_iv (iv)
1494 defs = ENCRYPTION_PARAMETERS.get (paramversion, None)
1496 raise FormatError ("header contains unknown parameter version %d; "
1497 "maybe the file was created by a more recent "
1498 "version of Deltatar" % paramversion)
1500 if enc == "aes-gcm":
1502 ( algorithms.AES (self.key)
1503 , modes.GCM (iv, tag=self.tag)
1504 , backend = default_backend ()) \
1506 elif enc == "passthrough":
1507 self.enc = PassthroughCipher ()
1509 raise InternalError ("encryption parameter set %d refers to unknown "
1510 "mode %r" % (paramversion, enc))
1511 self.set_object_counter (self.cnt + 1)
1514 def done (self, tag=None):
1516 Stop decryption of the current object and finalize it with the active
1517 context. This will throw an *InvalidGCMTag* exception to indicate that
1518 the authentication tag does not match the data. If the tag is correct,
1519 the rest of the plaintext is returned.
1524 data = self.enc.finalize ()
1526 if isinstance (tag, bytes) is False:
1527 raise InvalidParameter ("done: wrong type of parameter "
1528 "tag: expected bytes, got %s"
1530 data = self.enc.finalize_with_tag (self.tag)
1531 except cryptography.exceptions.InvalidTag:
1532 raise InvalidGCMTag ("done: tag mismatch of object %d: %s "
1533 "rejected by finalize ()"
1534 % (self.cnt, binascii.hexlify (self.tag)))
1535 self.ptsize += len (data)
1536 self.stats ["out"] += len (data)
1538 assert self.ctsize == self.ptsize == self.hdr_ctsize
1543 def process (self, buf):
1545 Decrypt the bytes object *buf* with the active decryptor.
1547 if isinstance (buf, bytes) is False:
1548 raise InvalidParameter ("process: expected byte buffer, not %s"
1550 self.ctsize += len (buf)
1551 if self.ctsize > self.hdr_ctsize:
1552 raise CiphertextTooLong ("process: object length exceeded: got "
1553 "%d B but header specfiies %d B"
1554 % (self.ctsize, self.hdr_ctsize))
1556 data = super().process (buf)
1557 self.ptsize += len (data)
1561 ###############################################################################
1563 ###############################################################################
1565 def _patch_global (glob, vow, n=None):
1567 Adapt upper file counter bound for testing IV logic. Completely unsafe.
1569 assert vow == "I am fully aware that this will void my warranty."
1570 r = globals () [glob]
1572 n = globals () [glob + "_DEFAULT"]
1573 globals () [glob] = n
1576 _testing_set_AES_GCM_IV_CNT_MAX = \
1577 partial (_patch_global, "AES_GCM_IV_CNT_MAX")
1579 _testing_set_PDTCRYPT_MAX_OBJ_SIZE = \
1580 partial (_patch_global, "PDTCRYPT_MAX_OBJ_SIZE")
1582 def open2_dump_file (fname, dir_fd, force=False):
1585 oflags = os.O_CREAT | os.O_WRONLY
1587 oflags |= os.O_TRUNC
1592 outfd = os.open (fname, oflags,
1593 stat.S_IRUSR | stat.S_IWUSR, dir_fd=dir_fd)
1594 except FileExistsError as exn:
1595 noise ("PDT: refusing to overwrite existing file %s" % fname)
1597 raise RuntimeError ("destination file %s already exists" % fname)
1598 if PDTCRYPT_VERBOSE is True:
1599 noise ("PDT: new output file %s (fd=%d)" % (fname, outfd))
1603 ###############################################################################
1604 ## freestanding invocation
1605 ###############################################################################
1607 PDTCRYPT_SUB_PROCESS = 0
1608 PDTCRYPT_SUB_SCRYPT = 1
1609 PDTCRYPT_SUB_SCAN = 2
1612 { "process" : PDTCRYPT_SUB_PROCESS
1613 , "scrypt" : PDTCRYPT_SUB_SCRYPT
1614 , "scan" : PDTCRYPT_SUB_SCAN }
1616 PDTCRYPT_DECRYPT = 1 << 0 # decrypt archive with password
1617 PDTCRYPT_SPLIT = 1 << 1 # split archive into individual objects
1618 PDTCRYPT_HASH = 1 << 2 # output scrypt hash for file and given password
1620 PDTCRYPT_SPLITNAME = "pdtcrypt-object-%d.bin"
1621 PDTCRYPT_RESCUENAME = "pdtcrypt-rescue-object-%0.5d.bin"
1623 PDTCRYPT_VERBOSE = False
1624 PDTCRYPT_STRICTIVS = False
1625 PDTCRYPT_OVERWRITE = False
1626 PDTCRYPT_BLOCKSIZE = 1 << 12
1631 PDTCRYPT_DEFAULT_VER = 1
1632 PDTCRYPT_DEFAULT_PVER = 1
1634 # scrypt hashing output control
1635 PDTCRYPT_SCRYPT_INTRANATOR = 0
1636 PDTCRYPT_SCRYPT_PARAMETERS = 1
1637 PDTCRYPT_SCRYPT_DEFAULT = PDTCRYPT_SCRYPT_INTRANATOR
1639 PDTCRYPT_SCRYPT_FORMAT = \
1640 { "i2n" : PDTCRYPT_SCRYPT_INTRANATOR
1641 , "params" : PDTCRYPT_SCRYPT_PARAMETERS }
1643 PDTCRYPT_TT_COLUMNS = 80 # assume standard terminal
1645 class PDTDecryptionError (Exception):
1646 """Decryption failed."""
1648 class PDTSplitError (Exception):
1649 """Decryption failed."""
1652 def noise (*a, **b):
1653 print (file=sys.stderr, *a, **b)
1656 class PassthroughDecryptor (object):
1658 curhdr = None # write current header on first data write
1660 def __init__ (self):
1661 if PDTCRYPT_VERBOSE is True:
1662 noise ("PDT: no encryption; data passthrough")
1664 def next (self, hdr):
1665 ok, curhdr = hdr_make (hdr)
1667 raise PDTDecryptionError ("bad header %r" % hdr)
1668 self.curhdr = curhdr
1671 if self.curhdr is not None:
1675 def process (self, d):
1676 if self.curhdr is not None:
1682 def depdtcrypt (mode, secret, ins, outs):
1684 Remove PDTCRYPT layer from all objects encrypted with the secret. Used on a
1685 Deltatar backup this will yield a (possibly Gzip compressed) tarball.
1687 ctleft = -1 # length of ciphertext to consume
1688 ctcurrent = 0 # total ciphertext of current object
1689 total_obj = 0 # total number of objects read
1690 total_pt = 0 # total plaintext bytes
1691 total_ct = 0 # total ciphertext bytes
1692 total_read = 0 # total bytes read
1693 outfile = None # Python file object for output
1695 if mode & PDTCRYPT_DECRYPT: # decryptor
1697 if ks == PDTCRYPT_SECRET_PW:
1698 decr = Decrypt (password=secret [1], strict_ivs=PDTCRYPT_STRICTIVS)
1699 elif ks == PDTCRYPT_SECRET_KEY:
1701 decr = Decrypt (key=key, strict_ivs=PDTCRYPT_STRICTIVS)
1703 raise InternalError ("‘%d’ does not specify a valid kind of secret"
1706 decr = PassthroughDecryptor ()
1709 """Dummy for non-split mode: output file does not vary."""
1712 if mode & PDTCRYPT_SPLIT:
1713 def nextout (outfile):
1715 We were passed an fd as outs for accessing the destination
1716 directory where extracted archive components are supposed
1721 if PDTCRYPT_VERBOSE is True:
1722 noise ("PDT: no output file to close at this point")
1724 if PDTCRYPT_VERBOSE is True:
1725 noise ("PDT: release output file %r" % outfile)
1726 # cleanup happens automatically by the GC; the next
1727 # line will error out on account of an invalid fd
1730 assert total_obj > 0
1731 fname = PDTCRYPT_SPLITNAME % total_obj
1733 outfd = open2_dump_file (fname, outs, force=PDTCRYPT_OVERWRITE)
1734 except RuntimeError as exn:
1735 raise PDTSplitError (exn)
1736 return os.fdopen (outfd, "wb", closefd=True)
1740 """ESPIPE is normal on non-seekable stdio stream."""
1743 except OSError as exn:
1744 if exn.errno == errno.ESPIPE:
1747 def out (pt, outfile):
1751 if PDTCRYPT_VERBOSE is True:
1752 noise ("PDT:\t· decrypt plaintext %d B" % (npt))
1754 nn = outfile.write (pt)
1755 except OSError as exn: # probably ENOSPC
1756 raise DecryptionError ("error (%s)" % exn)
1758 raise DecryptionError ("write aborted after %d of %d B" % (nn, npt))
1762 # current object completed; in a valid archive this marks either
1763 # the start of a new header or the end of the input
1764 if ctleft == 0: # current object requires finalization
1765 if PDTCRYPT_VERBOSE is True:
1766 noise ("PDT: %d finalize" % tell (ins))
1769 except InvalidGCMTag as exn:
1770 raise DecryptionError ("error finalizing object %d (%d B): "
1771 "%r" % (total_obj, len (pt), exn)) \
1774 if PDTCRYPT_VERBOSE is True:
1775 noise ("PDT:\t· object validated")
1777 if PDTCRYPT_VERBOSE is True:
1778 noise ("PDT: %d hdr" % tell (ins))
1780 hdr = hdr_read_stream (ins)
1781 total_read += PDTCRYPT_HDR_SIZE
1782 except EndOfFile as exn:
1783 total_read += exn.remainder
1784 if total_ct + total_obj * PDTCRYPT_HDR_SIZE != total_read:
1785 raise PDTDecryptionError ("ciphertext processed (%d B) plus "
1786 "overhead (%d × %d B) does not match "
1787 "the number of bytes read (%d )"
1788 % (total_ct, total_obj, PDTCRYPT_HDR_SIZE,
1790 # the single good exit
1791 return total_read, total_obj, total_ct, total_pt
1792 except InvalidHeader as exn:
1793 raise PDTDecryptionError ("invalid header at position %d in %r "
1794 "(%s)" % (tell (ins), exn, ins))
1795 if PDTCRYPT_VERBOSE is True:
1796 pretty = hdr_fmt_pretty (hdr)
1797 noise (reduce (lambda a, e: (a + "\n" if a else "") + "PDT:\t· " + e,
1798 pretty.splitlines (), ""))
1799 ctcurrent = ctleft = hdr ["ctsize"]
1803 total_obj += 1 # used in file counter with split mode
1805 # finalization complete or skipped in case of first object in
1806 # stream; create a new output file if necessary
1807 outfile = nextout (outfile)
1809 if PDTCRYPT_VERBOSE is True:
1810 noise ("PDT: %d decrypt obj no. %d, %d B"
1811 % (tell (ins), total_obj, ctleft))
1813 # always allocate a new buffer since python-cryptography doesn’t allow
1814 # passing a bytearray :/
1815 nexpect = min (ctleft, PDTCRYPT_BLOCKSIZE)
1816 if PDTCRYPT_VERBOSE is True:
1817 noise ("PDT:\t· [%d] %d%% done, read block (%d B of %d B remaining)"
1819 100 - ctleft * 100 / (ctcurrent > 0 and ctcurrent or 1),
1821 ct = ins.read (nexpect)
1825 raise EndOfFile (nct,
1826 "hit EOF after %d of %d B in block [%d:%d); "
1827 "%d B ciphertext remaining for object no %d"
1828 % (nct, nexpect, off, off + nexpect, ctleft,
1834 if PDTCRYPT_VERBOSE is True:
1835 noise ("PDT:\t· decrypt ciphertext %d B" % (nct))
1836 pt = decr.process (ct)
1840 def deptdcrypt_mk_stream (kind, path):
1841 """Create stream from file or stdio descriptor."""
1842 if kind == PDTCRYPT_SINK:
1844 if PDTCRYPT_VERBOSE is True: noise ("PDT: sink: stdout")
1845 return sys.stdout.buffer
1847 if PDTCRYPT_VERBOSE is True: noise ("PDT: sink: file %s" % path)
1848 return io.FileIO (path, "w")
1849 if kind == PDTCRYPT_SOURCE:
1851 if PDTCRYPT_VERBOSE is True: noise ("PDT: source: stdin")
1852 return sys.stdin.buffer
1854 if PDTCRYPT_VERBOSE is True: noise ("PDT: source: file %s" % path)
1855 return io.FileIO (path, "r")
1857 raise ValueError ("bogus stream “%s” / %s" % (kind, path))
1860 def mode_depdtcrypt (mode, secret, ins, outs):
1862 total_read, total_obj, total_ct, total_pt = \
1863 depdtcrypt (mode, secret, ins, outs)
1864 except DecryptionError as exn:
1865 noise ("PDT: Decryption failed:")
1867 noise ("PDT: “%s”" % exn)
1869 noise ("PDT: Did you specify the correct key / password?")
1872 except PDTSplitError as exn:
1873 noise ("PDT: Split operation failed:")
1875 noise ("PDT: “%s”" % exn)
1877 noise ("PDT: Hint: target directory should be empty.")
1881 if PDTCRYPT_VERBOSE is True:
1882 noise ("PDT: decryption successful" )
1883 noise ("PDT: %.10d bytes read" % total_read)
1884 noise ("PDT: %.10d objects decrypted" % total_obj )
1885 noise ("PDT: %.10d bytes ciphertext" % total_ct )
1886 noise ("PDT: %.10d bytes plaintext" % total_pt )
1892 def mode_scrypt (pw, ins=None, nacl=None, fmt=PDTCRYPT_SCRYPT_INTRANATOR):
1894 paramversion = PDTCRYPT_DEFAULT_PVER
1896 hsh, nacl, version, paramversion = scrypt_hashsource (pw, ins)
1897 defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
1899 nacl = binascii.unhexlify (nacl)
1900 defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
1901 version = PDTCRYPT_DEFAULT_VER
1903 kdfname, params = defs ["kdf"]
1905 kdf = kdf_by_version (None, defs)
1906 hsh, _void = kdf (pw, nacl)
1910 if fmt == PDTCRYPT_SCRYPT_INTRANATOR:
1911 out = json.dumps ({ "salt" : base64.b64encode (nacl).decode ()
1912 , "key" : base64.b64encode (hsh) .decode ()
1913 , "paramversion" : paramversion })
1914 elif fmt == PDTCRYPT_SCRYPT_PARAMETERS:
1915 out = json.dumps ({ "salt" : binascii.hexlify (nacl).decode ()
1916 , "key" : binascii.hexlify (hsh) .decode ()
1917 , "version" : version
1918 , "scrypt_params" : { "N" : params ["N"]
1919 , "r" : params ["r"]
1920 , "p" : params ["p"]
1921 , "dkLen" : params ["dkLen"] } })
1923 raise RuntimeError ("bad scrypt output scheme %r" % fmt)
1928 def noise_output_candidates (cands, indent=8, cols=PDTCRYPT_TT_COLUMNS):
1930 Print a list of offsets without garbling the terminal too much.
1932 The indent is counted from column zero; if it is wide enough, the “PDT: ”
1933 marker will be prepended, considered part of the indentation.
1937 idt = " " * indent if indent < 5 else "PDT: " + " " * (indent - 5)
1942 init = True # prevent leading separator
1945 raise ValueError ("the requested indentation exceeds the line "
1946 "width by %d" % (indent - wd))
1956 if lpos > wd: # line break
1972 SLICE_START = 1 # ordering is important to have starts of intervals
1973 SLICE_END = 0 # sorted before equal ends
1975 def find_overlaps (slices):
1977 Find overlapping slices: iterate open/close points of intervals, tracking
1978 the ones open at any time.
1981 inside = set () # of indices into bounds
1982 ovrlp = set () # of indices into bounds
1984 for i, s in enumerate (slices):
1985 bounds.append ((s [0], SLICE_START, i))
1986 bounds.append ((s [1], SLICE_END , i))
1987 bounds = sorted (bounds)
1991 if val [1] == SLICE_START:
1994 if len (inside) > 1: # closing one that overlapped
1998 return [ slices [i] for i in ovrlp ]
2001 def mode_scan (secret, fname, outs=None, nacl=None):
2003 Dissect a binary file, looking for PDTCRYPT headers and objects.
2005 If *outs* is supplied, recoverable data will be dumped into the specified
2009 ifd = os.open (fname, os.O_RDONLY)
2010 except FileNotFoundError:
2011 noise ("PDT: failed to open %s readonly" % fname)
2016 if PDTCRYPT_VERBOSE is True:
2017 noise ("PDT: scan for potential sync points")
2018 cands = locate_hdr_candidates (ifd)
2019 if len (cands) == 0:
2020 noise ("PDT: scan complete: input does not contain potential PDT "
2021 "headers; giving up.")
2023 if PDTCRYPT_VERBOSE is True:
2024 noise ("PDT: scan complete: found %d candidates:" % len (cands))
2025 noise_output_candidates (cands)
2030 junk, todo, slices = [], [], []
2035 vdt, hdr = inspect_hdr (ifd, cand)
2037 vdts = verdict_fmt (vdt)
2039 if vdt == HDR_CAND_JUNK:
2040 noise ("PDT: obj %d: %s object: bad header, skipping" % vdts)
2043 off0 = cand + PDTCRYPT_HDR_SIZE
2044 if PDTCRYPT_VERBOSE is True:
2045 noise ("PDT: obj %d: read payload @%d" % (nobj, off0))
2046 pretty = hdr_fmt_pretty (hdr)
2047 noise (reduce (lambda a, e: (a + "\n" if a else "") + "PDT:\t· " + e,
2048 pretty.splitlines (), ""))
2051 if outs is not None:
2052 ofname = PDTCRYPT_RESCUENAME % nobj
2053 ofd = open2_dump_file (ofname, outs, force=PDTCRYPT_OVERWRITE)
2055 ctsize = hdr ["ctsize"]
2057 l = try_decrypt (ifd, off0, hdr, secret, ofd=ofd)
2059 slices.append ((off0, off0 + l))
2063 if vdt == HDR_CAND_GOOD and ok is True:
2064 noise ("PDT: %d → ✓ %s object %d–%d"
2065 % (cand, vdts, off0, off0 + ctsize))
2066 elif vdt == HDR_CAND_FISHY and ok is True:
2067 noise ("PDT: %d → × %s object %d–%d, corrupt header"
2068 % (cand, vdts, off0, off0 + ctsize))
2069 elif vdt == HDR_CAND_GOOD and ok is False:
2070 noise ("PDT: %d → × %s object %d–%d, problematic payload"
2071 % (cand, vdts, off0, off0 + ctsize))
2072 elif vdt == HDR_CAND_FISHY and ok is False:
2073 noise ("PDT: %d → × %s object %d–%d, corrupt header, problematic "
2074 "ciphertext" % (cand, vdts, off0, off0 + ctsize))
2081 noise ("PDT: all headers ok")
2083 noise ("PDT: %d candidates not parseable as headers:" % len (junk))
2084 noise_output_candidates (junk)
2086 overlap = find_overlaps (slices)
2087 if len (overlap) > 0:
2088 noise ("PDT: %d objects overlapping others" % len (overlap))
2089 for slice in overlap:
2090 noise ("PDT: × %d→%d" % (slice [0], slice [1]))
2092 def usage (err=False):
2096 indent = ' ' * len (SELF)
2097 out ("usage: %s SUBCOMMAND { --help" % SELF)
2098 out (" %s | [ -v ] { -p PASSWORD | -k KEY }" % indent)
2099 out (" %s [ { -i | --in } { - | SOURCE } ]" % indent)
2100 out (" %s [ { -n | --nacl } { SALT } ]" % indent)
2101 out (" %s [ { -o | --out } { - | DESTINATION } ]" % indent)
2102 out (" %s [ -D | --no-decrypt ] [ -S | --split ]" % indent)
2103 out (" %s [ -f | --format ]" % indent)
2106 out ("\t\tSUBCOMMAND main mode: { process | scrypt }")
2108 out ("\t\t process: extract objects from PDT archive")
2109 out ("\t\t scrypt: calculate hash from password and first object")
2110 out ("\t\t-p PASSWORD password to derive the encryption key from")
2111 out ("\t\t-k KEY encryption key as 16 bytes in hexadecimal notation")
2112 out ("\t\t-s enforce strict handling of initialization vectors")
2113 out ("\t\t-i SOURCE file name to read from")
2114 out ("\t\t-o DESTINATION file to write output to")
2115 out ("\t\t-n SALT provide salt for scrypt mode in hex encoding")
2116 out ("\t\t-v print extra info")
2117 out ("\t\t-S split into files at object boundaries; this")
2118 out ("\t\t requires DESTINATION to refer to directory")
2119 out ("\t\t-D PDT header and ciphertext passthrough")
2120 out ("\t\t-f format of SCRYPT hash output (“default” or “parameters”)")
2122 out ("\tinstead of filenames, “-” may used to specify stdin / stdout")
2124 sys.exit ((err is True) and 42 or 0)
2134 def parse_argv (argv):
2135 global PDTCRYPT_OVERWRITE
2137 mode = PDTCRYPT_DECRYPT
2143 scrypt_format = PDTCRYPT_SCRYPT_DEFAULT
2146 SELF = os.path.basename (next (argvi))
2149 rawsubcmd = next (argvi)
2150 subcommand = PDTCRYPT_SUB [rawsubcmd]
2151 except StopIteration:
2152 bail ("ERROR: subcommand required")
2154 bail ("ERROR: invalid subcommand “%s” specified" % rawsubcmd)
2160 except StopIteration:
2161 bail ("ERROR: argument list incomplete")
2163 def checked_secret (s):
2168 bail ("ERROR: encountered “%s” but secret already given" % arg)
2171 if arg in [ "-h", "--help" ]:
2174 elif arg in [ "-v", "--verbose", "--wtf" ]:
2175 global PDTCRYPT_VERBOSE
2176 PDTCRYPT_VERBOSE = True
2177 elif arg in [ "-i", "--in", "--source" ]:
2178 insspec = checked_arg ()
2179 if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt from %s" % insspec)
2180 elif arg in [ "-p", "--password" ]:
2181 arg = checked_arg ()
2182 checked_secret (make_secret (password=arg))
2183 if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypting with password")
2185 if subcommand == PDTCRYPT_SUB_PROCESS:
2186 if arg in [ "-s", "--strict-ivs" ]:
2187 global PDTCRYPT_STRICTIVS
2188 PDTCRYPT_STRICTIVS = True
2189 elif arg in [ "-o", "--out", "--dest", "--sink" ]:
2190 outsspec = checked_arg ()
2191 if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
2192 elif arg in [ "-f", "--force" ]:
2193 PDTCRYPT_OVERWRITE = True
2194 if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
2195 elif arg in [ "-S", "--split" ]:
2196 mode |= PDTCRYPT_SPLIT
2197 if PDTCRYPT_VERBOSE is True: noise ("PDT: split files")
2198 elif arg in [ "-D", "--no-decrypt" ]:
2199 mode &= ~PDTCRYPT_DECRYPT
2200 if PDTCRYPT_VERBOSE is True: noise ("PDT: not decrypting")
2201 elif arg in [ "-k", "--key" ]:
2202 arg = checked_arg ()
2203 checked_secret (make_secret (key=arg))
2204 if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypting with key")
2206 bail ("ERROR: unexpected positional argument “%s”" % arg)
2207 elif subcommand == PDTCRYPT_SUB_SCRYPT:
2208 if arg in [ "-n", "--nacl", "--salt" ]:
2209 nacl = checked_arg ()
2210 if PDTCRYPT_VERBOSE is True: noise ("PDT: salt key with %s" % nacl)
2211 elif arg in [ "-f", "--format" ]:
2212 arg = checked_arg ()
2214 scrypt_format = PDTCRYPT_SCRYPT_FORMAT [arg]
2216 bail ("ERROR: invalid scrypt output format %s" % arg)
2217 if PDTCRYPT_VERBOSE is True:
2218 noise ("PDT: scrypt output format “%s”" % scrypt_format)
2220 bail ("ERROR: unexpected positional argument “%s”" % arg)
2221 elif subcommand == PDTCRYPT_SUB_SCAN:
2222 if arg in [ "-o", "--out", "--dest", "--sink" ]:
2223 outsspec = checked_arg ()
2224 if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
2225 elif arg in [ "-f", "--force" ]:
2226 PDTCRYPT_OVERWRITE = True
2227 if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
2229 bail ("ERROR: unexpected positional argument “%s”" % arg)
2232 if PDTCRYPT_VERBOSE is True:
2233 noise ("ERROR: no password or key specified, trying $PDTCRYPT_PASSWORD")
2234 epw = os.getenv ("PDTCRYPT_PASSWORD")
2236 checked_secret (make_secret (password=epw.strip ()))
2239 if PDTCRYPT_VERBOSE is True:
2240 noise ("ERROR: no password or key specified, trying $PDTCRYPT_KEY")
2241 ek = os.getenv ("PDTCRYPT_KEY")
2243 checked_secret (make_secret (key=ek.strip ()))
2246 if subcommand == PDTCRYPT_SUB_SCRYPT:
2247 bail ("ERROR: scrypt hash mode requested but no password given")
2248 elif mode & PDTCRYPT_DECRYPT:
2249 bail ("ERROR: decryption requested but no password given")
2251 if mode & PDTCRYPT_SPLIT and outsspec is None:
2252 bail ("ERROR: split mode is incompatible with stdout sink "
2255 if subcommand == PDTCRYPT_SUB_SCAN and outsspec is None:
2256 pass # no output by default in scan mode
2257 elif mode & PDTCRYPT_SPLIT or subcommand == PDTCRYPT_SUB_SCAN:
2258 # destination must be directory
2260 bail ("ERROR: mode is incompatible with stdout sink")
2263 os.makedirs (outsspec, 0o700)
2264 except FileExistsError:
2265 # if it’s a directory with appropriate perms, everything is
2266 # good; otherwise, below invocation of open(2) will fail
2268 outs = os.open (outsspec, os.O_DIRECTORY, 0o600)
2269 except FileNotFoundError as exn:
2270 bail ("ERROR: cannot create target directory “%s”" % outsspec)
2271 except NotADirectoryError as exn:
2272 bail ("ERROR: target path “%s” is not a directory" % outsspec)
2274 outs = deptdcrypt_mk_stream (PDTCRYPT_SINK, outsspec or "-")
2276 if subcommand == PDTCRYPT_SUB_SCAN:
2278 bail ("ERROR: please supply an input file for scanning")
2280 bail ("ERROR: input must be seekable; please specify a file")
2281 return True, partial (mode_scan, secret, insspec, outs, nacl=nacl)
2283 if subcommand == PDTCRYPT_SUB_SCRYPT:
2284 if secret [0] == PDTCRYPT_SECRET_KEY:
2285 bail ("ERROR: scrypt mode requires a password")
2286 if insspec is not None and nacl is not None \
2287 or insspec is None and nacl is None :
2288 bail ("ERROR: please supply either an input file or "
2293 if insspec is not None or subcommand != PDTCRYPT_SUB_SCRYPT:
2294 ins = deptdcrypt_mk_stream (PDTCRYPT_SOURCE, insspec or "-")
2296 if subcommand == PDTCRYPT_SUB_SCRYPT:
2297 return True, partial (mode_scrypt, secret [1].encode (), ins, nacl,
2300 return True, partial (mode_depdtcrypt, mode, secret, ins, outs)
2304 ok, runner = parse_argv (argv)
2306 if ok is True: return runner ()
2311 if __name__ == "__main__":
2312 sys.exit (main (sys.argv))