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