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