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