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