__all__ = [ "hdr_make", "hdr_read", "hdr_fmt", "hdr_fmt_pretty"
- , "PDTCRYPT_HDR_SIZE" , "AES_GCM_IV_CNT_DATA", "AES_GCM_IV_CNT_INFOFILE"
+ , "PDTCRYPT_HDR_SIZE", "AES_GCM_IV_CNT_DATA"
+ , "AES_GCM_IV_CNT_INFOFILE", "AES_GCM_IV_CNT_INDEX"
]
AES_GCM_MAX_SIZE = (1 << 36) - (1 << 5) # 2^39 - 2^8 b ≅ 64 GB
AES_GCM_FMT_TAG = "<16s"
+# index and info files are written on-the fly while encrypting so their
+# counters must be available inadvance
AES_GCM_IV_CNT_INFOFILE = 1 # constant
-AES_GCM_IV_CNT_DATA = AES_GCM_IV_CNT_INFOFILE + 1 # also for multivolume
+AES_GCM_IV_CNT_INDEX = AES_GCM_IV_CNT_INFOFILE + 1
+AES_GCM_IV_CNT_DATA = AES_GCM_IV_CNT_INDEX + 1 # also for multivolume
AES_GCM_IV_CNT_MAX = 0xffFFffFF
+ (len (b) | 1 == len (b) and " %.2x" % b[-1] or "") # odd lengths
+def hdr_iv_counter (h):
+ """Extract the variable part of the IV of the given header."""
+ _fixed, cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
+ return cnt
+
+
+def hdr_iv_fixed (h):
+ """Extract the fixed part of the IV of the given header."""
+ fixed, _cnt = struct.unpack (FMT_I2N_IV, h ["iv"])
+ return fixed
+
+
hdr_dump = hex_spaced_of_bytes
return partial (fn, params)
-STATE_FRESH = 0
-STATE_DEAD = 1
-STATE_LIVE = 2
-
class Crypto (object):
"""
Encryption context to remain alive throughout an entire tarfile pass.
cnt = None # file counter (uint32_t != 0)
iv = None # current IV
fixed = None # accu for 64 bit fixed parts of IV
+ used_ivs = None # tracks IVs during decryption
password = None
paramversion = None
stats = { "in" : 0
ctsize = -1
ptsize = -1
- info_counter_used = False
+ info_counter_used = False
+ index_counter_used = False
def __init__ (self, *al, **akv):
self.set_parameters (*al, **akv)
raise InvalidParameter ("attempted to reuse info file counter "
"%d: must be unique" % cnt)
self.info_counter_used = True
- return
+ elif cnt == AES_GCM_IV_CNT_INDEX:
+ if self.index_counter_used is True:
+ raise InvalidParameter ("attempted to reuse index file counter "
+ "%d: must be unique" % cnt)
+ self.index_counter_used = True
if cnt <= AES_GCM_IV_CNT_MAX:
self.cnt = cnt
return
if isinstance (filename, str) is False:
raise InvalidParameter ("next: filename must be a string, no %s"
% type (filename))
- if counter is not None \
- and isinstance (counter, int) is False:
- raise InvalidParameter ("next: the supplied counter is of invalid "
- "type %s; please pass an integer instead"
- % type (counter))
+ if counter is not None:
+ if isinstance (counter, int) is False:
+ raise InvalidParameter ("next: the supplied counter is of "
+ "invalid type %s; please pass an "
+ "integer instead" % type (counter))
+ self.set_object_counter (counter)
self.iv = self.iv_make ()
if self.paramenc == "aes-gcm":
self.enc = Cipher \
self.lastinfo = (filename, hdrdum)
super().next (self.password, self.paramversion, self.nacl)
- self.set_object_counter (counter if counter is not None else self.cnt + 1)
+ self.set_object_counter (self.cnt + 1)
return hdrdum
class Decrypt (Crypto):
- tag = None # GCM tag, part of header
- used_ivs = None # if a set, panic on duplicate object IV
- last_iv = None # check consecutive ivs in strict mode
+ tag = None # GCM tag, part of header
+ strict_ivs = False # if True, panic on duplicate object IV
+ last_iv = None # check consecutive ivs in strict mode
def __init__ (self, password, counter=None, fixedparts=None,
strict_ivs=False):
self.fixed.sort ()
super().__init__ (password, counter=counter)
- if strict_ivs is True:
- self.used_ivs = set ()
+ self.used_ivs = set ()
+ self.strict_ivs = strict_ivs
super().__init__ (password, counter=counter)
def check_duplicate_iv (self, iv):
- if iv in self.used_ivs:
+ if self.strict_ivs is True and iv in self.used_ivs:
raise DuplicateIV ("iv [%r] was reused" % iv)
# vi has not been used before; add to collection
self.used_ivs.add (iv)
def check_consecutive_iv (self, iv):
fixed, cnt = struct.unpack (FMT_I2N_IV, iv)
- if self.last_iv is not None \
+ if self.strict_ivs is True \
+ and self.last_iv is not None \
and self.last_iv [0] == fixed \
and self.last_iv [1] != cnt - 1:
raise NonConsecutiveIV ("iv [%r] counter not successor of "
fixed, _ = struct.unpack (FMT_I2N_IV, iv)
raise InvalidIVFixedPart ("iv [%r] has invalid fixed part [%r]"
% (iv, fixed))
- if self.used_ivs is not None:
- self.check_duplicate_iv (iv)
- self.check_consecutive_iv (iv)
+ self.check_duplicate_iv (iv)
+ self.check_consecutive_iv (iv)
self.tag = tag
defs = ENCRYPTION_PARAMETERS.get (paramversion, None)
enccounter = None
if mode == "w":
crypto_ctx = self.initialize_encryption (CRYPTO_MODE_ENCRYPT)
- if crypto_ctx is not None:
- if kind == AUXILIARY_FILE_INFO:
- enccounter = crypto.AES_GCM_IV_CNT_INFOFILE
- elif kind == AUXILIARY_FILE_INDEX:
- enccounter = crypto.AES_GCM_IV_CNT_INDEX
- else:
- raise Exception ("invalid kind of aux file %r" % kind)
elif mode == "r":
crypto_ctx = self.initialize_encryption (CRYPTO_MODE_DECRYPT)
+ if crypto_ctx is not None:
+ if kind == AUXILIARY_FILE_INFO:
+ enccounter = crypto.AES_GCM_IV_CNT_INFOFILE
+ elif kind == AUXILIARY_FILE_INDEX:
+ enccounter = crypto.AES_GCM_IV_CNT_INDEX
+ else:
+ raise Exception ("invalid kind of aux file %r" % kind)
+
sink = tarfile._Stream(name=path, mode=mode, comptype=comptype,
bufsize=tarfile.RECORDSIZE, fileobj=None,
encryption=crypto_ctx, enccounter=enccounter)
volume_number=0)
tarfile_path = os.path.join(backup_path, vol_name)
- # postpone creation of index file to accomodate encryption
- index_accu = io.BytesIO ()
-
# init index
cwd = os.getcwd()
+ index_name = self.index_name_func(is_full=False)
+ index_path = os.path.join(backup_path, index_name)
+ index_sink = self.open_auxiliary_file(index_path, 'w')
+
def new_volume_handler(deltarobj, cwd, backup_path, tarobj, base_name, volume_number):
'''
Handles the new volumes
# wraps some args from context into the handler
new_volume_handler = partial(new_volume_handler, self, cwd, backup_path)
- index_accu.write(bytes('{"type": "python-delta-tar-index", "version": 1, "backup-type": "diff", "extra_data": %s}\n' % extra_data_str, 'UTF-8'))
+ index_sink.write(bytes('{"type": "python-delta-tar-index", "version": 1, "backup-type": "diff", "extra_data": %s}\n' % extra_data_str, 'UTF-8'))
s = bytes('{"type": "BEGIN-FILE-LIST"}\n', 'UTF-8')
# calculate checksum and write into the stream
crc = binascii.crc32(s) & 0xFFFFffff
- index_accu.write(s)
+ index_sink.write(s)
# start creating the tarfile
tarobj = tarfile.TarFile.open(tarfile_path,
# store the stat dict in the index
s = bytes(json.dumps(stat) + '\n', 'UTF-8')
crc = binascii.crc32(s, crc) & 0xffffffff
- index_accu.write(s)
+ index_sink.write(s)
s = bytes('{"type": "END-FILE-LIST"}\n', 'UTF-8')
crc = binascii.crc32(s, crc) & 0xffffffff
- index_accu.write(s)
+ index_sink.write(s)
s = bytes('{"type": "file-list-checksum", "checksum": %d}\n' % crc, 'UTF-8')
- index_accu.write(s)
+ index_sink.write(s)
index_it.release()
os.chdir(cwd)
tarobj.close()
-
- index_name = self.index_name_func(is_full=False)
- index_path = os.path.join(backup_path, index_name)
- index_sink = self.open_auxiliary_file(index_path, 'w')
- index_sink.write(index_accu.getvalue ())
index_sink.close()
index_it.release()
os.chdir(cwd)
helper.cleanup()
+ helper.validate()
def _parse_json_line(self, f, l_no):
'''
self._directories = []
self._deltatar = deltatar
self._cwd = cwd
- self.password = deltatar.password
+ self._password = deltatar.password
+ self._decryptors = []
try:
import grp, pwd
is_full = index == index_list[-1]
decryptor = None
- if self.password is not None:
- decryptor = crypto.Decrypt (self.password)
+ if self._password is not None:
+ decryptor = crypto.Decrypt (self._password)
# make paths absolute to avoid cwd problems
if not os.path.isabs(index):
)
self._data.append(s)
+
+ def validate (self):
+ """If encryption was used, verify post-conditions."""
+ if len (self._decryptors) == 0:
+ return
+ acc = None
+ for dec in self._decryptors:
+ if acc is None:
+ acc = dec.used_ivs.copy ()
+ else:
+ used_ivs = dec.used_ivs
+ intersect = used_ivs & acc
+ if len (intersect) > 0:
+ raise Exception ("ERROR: %d duplicate IVs found during "
+ "decryption" % len (intersect))
+ acc |= used_ivs
+
+
def cleanup(self):
'''
Closes all open files
A stream-like object could be for example: sys.stdin,
sys.stdout, a socket, a tape device etc.
- _Stream is intended to be used only internally.
+ _Stream is intended to be used only internally but is
+ nevertherless used externally by Deltatar.
+
+ When encrypting, the ``enccounter`` will be used for
+ initializing the first cryptographic context. When
+ decrypting, its value will be compared to the decrypted
+ object. Decryption fails if the value does not match.
+ In effect, this means that a ``_Stream`` whose ctor was
+ passed ``enccounter`` can only be used to encrypt or
+ decrypt a single object.
"""
remainder = -1 # track size in encrypted entries
if comptype == '':
comptype = "tar"
+ self.enccounter = None
+ if self.arcmode & ARCMODE_ENCRYPT:
+ self.enccounter = enccounter
+
self.name = name or ""
self.mode = mode
self.comptype = comptype
elif mode == "w":
if not (self.arcmode & ARCMODE_CONCAT):
if self.arcmode & ARCMODE_ENCRYPT:
- self._init_write_encrypt (name, enccounter)
+ self._init_write_encrypt (name)
self._init_write_gz ()
self.crc = zlib.crc32(b"") & 0xFFFFffff
if not (self.arcmode & ARCMODE_CONCAT) \
and mode == "w" \
and self.arcmode & ARCMODE_ENCRYPT:
- self._init_write_encrypt (name, enccounter)
+ self._init_write_encrypt (name)
else:
if self.arcmode & ARCMODE_ENCRYPT:
"processing %r at pos %d"
% (exn, self.fileobj, lasthdr)) \
from exn
+ if self.enccounter is not None:
+ # enforce that the iv counter in the header matches an
+ # explicitly requested one
+ iv = crypto.hdr_iv_counter (hdr)
+ if iv != self.enccounter:
+ raise DecryptionError ("expected IV counter %d, got %d"
+ % (self.enccounter, iv))
self.lasthdr = lasthdr
self.remainder = hdr ["ctsize"] # distance to next header
self.encryption.next (hdr)