From 1d8f9cadfee9bc9936a8d802944e7e325a725c3d Mon Sep 17 00:00:00 2001 From: Philipp Gesang Date: Fri, 3 Mar 2017 17:57:37 +0100 Subject: [PATCH] supersede encryption type by encryption parameters WIP --- deltatar/crypto.py | 130 +++++++++++++++++++++++++++++++++++--------------- deltatar/deltatar.py | 6 +- deltatar/tarfile.py | 128 ++++++++++++++++++++++++++++++------------------- 3 files changed, 174 insertions(+), 90 deletions(-) diff --git a/deltatar/crypto.py b/deltatar/crypto.py index f4e678c..cc57c7d 100755 --- a/deltatar/crypto.py +++ b/deltatar/crypto.py @@ -56,6 +56,21 @@ __all__ = [ "ENCRYPT", "DECRYPT" , "hdr_make", "hdr_read", "hdr_fmt", "hdr_fmt_pretty" , "I2N_HDR_SIZE" ] + +############################################################################### +## crypto layer version +############################################################################### + +ENCRYPTION_PARAMETERS = \ + { 1: \ + { "kdf": ("scrypt", + { "dkLen" : 16 + , "N" : 1 << 15 + , "r" : 8 + , "p" : 1 + , "NaCl_LEN" : 16 }) } } + + ############################################################################### ## constants ############################################################################### @@ -144,24 +159,27 @@ def hdr_read (data): } -def hdr_make (hdr): +def hdr_from_params (version, paramversion, nacl, iv, ctsize): buf = bytearray (I2N_HDR_SIZE) bufv = memoryview (buf) try: struct.pack_into (FMT_I2N_HDR, bufv, 0, I2N_HDR_MAGIC, - hdr["version"], - hdr["paramversion"], - hdr["nacl"], - hdr["iv"], - hdr["ctsize"]) + version, paramversion, nacl, iv, ctsize) except Exception as exn: return False, "error writing header: %s" % str (exn) return True, bytes (buf) +def hdr_make (hdr): + return hdr_from_params (version=hdr.get("version"), + paramversion=hdr.get("paramversion"), + nacl=hdr.get("nacl"), iv=hdr.get("iv"), + ctsize=hdr.get("ctsize")) + + HDR_FMT = "I2n_header { version: %d, paramversion: %d, nacl: %s[%d]," \ " iv: %s[%d], ctsize: %d }" @@ -215,28 +233,6 @@ def tag_read (data): ############################################################################### -def aesgcm_enc (key, data, aad, iv=None): - iv = iv or os.urandom(AES_GCM_IV_LEN) - enc = Cipher \ - ( algorithms.AES (key) - , modes.GCM (iv) - , backend = default_backend ()) \ - .encryptor () - enc.authenticate_additional_data (aad) - result = enc.update (data) + enc.finalize () - return iv, result, enc.tag - - -def aesgcm_dec (key, data, aad, iv, tag): - dec = Cipher \ - ( algorithms.AES (key) - , modes.GCM (iv) - , backend = default_backend ()) \ - . decryptor () - dec.authenticate_additional_data (aad) - return dec.update (data) + dec.finalize_with_tag (tag) - - ENCRYPT = 0 DECRYPT = 1 @@ -263,12 +259,10 @@ class AES_GCM_context (object): Thin wrapper context over AES encryption. """ - key = None - aad = None + ctx = None iv = None def __init__ (self, kind, key, aad, iv=None): - self.key = key self.aad = aad if not iv: iv = os.urandom (AES_GCM_IV_LEN) @@ -284,22 +278,82 @@ class AES_GCM_context (object): def done (self, tag=None): if self.ctx is None: - return False, "no valid encryption context" + return False, "no valid encryption context", None if tag is None: ret = self.ctx.finalize () return True, ret, self.ctx.tag - ret = self.ctx.finalize_with_tag (tag) + ret = self.ctx.finalize_with_tag (tag) # XXX this fails if tags don’t match return True, ret, None ############################################################################### -## keys +## convenience wrapper ############################################################################### -def scrypt_derive (pw, NaCl=None): - NaCl = NaCl or os.urandom (SCRYPT_NaCl_LEN) - return NaCl, \ - pylibscrypt.scrypt (pw, NaCl, SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_dkLen) + +class Crypto (object): + """ + Encryption context to remain alive throughout an entire tarfile pass. + """ + kind = None + aes = None + nacl = None + key = None + pfx = None # 64 bit fixed parts of IV + cnt = None + + def __init__ (self, kind, pw, nacl, paramversion, pfx=None): + defs = ENCRYPTION_PARAMETERS.get(paramversion) + if defs is None: + raise ValueError ("no encryption parameters for version %r" + % paramversion) + (kdf, params) = defs["hash"] + if kdf != "scrypt": + raise ValueError ("key derivation method %r unknown" % kdf) + if nacl is None: # XXX do we actually want this anywhere? + nacl = os.urandom (params["NaCl_LEN"]) + self.key = pylibscrypt.scrypt (pw, nacl, params["N"], params["r"], + params["p"], params["dkLen"]) + self.kind = kind + self.cnt = 1 + self.pfx = [ pfx or os.urandom(8) ] + + + def iv_make (self): + return struct.pack("<8sL", self.pfx, self.cnt % 0xffFFffFF) + + + def next (self, aad=None): + """Set up encryption for new object in stream. + + if ``aad is None``: end reached, no new context. + """ + if self.aes is not None: + (ok, data, tag) = self.aes.done () + if ok is False: + raise + else: + data, tag = None, None + + if aad is None: + return + + self.cnt += 1 + try: + self.aes = AES_GCM_context (self.kind, self.key, aad, iv=iv_make()) + except Exception as exn: + # XXX devise some error handling strategy + raise ("write failed with buffer of size %d" % len(buf)) + + return data, tag + + + def process (self, buf): + (ok, res) = self.aes.process_chunk (buf) + if ok is True: + return res + # XXX not clear how tarfile expects to communicate this + raise IOError ("write failed with buffer of size %d" % len(buf)) ############################################################################### diff --git a/deltatar/deltatar.py b/deltatar/deltatar.py index 6ad4561..27033a8 100644 --- a/deltatar/deltatar.py +++ b/deltatar/deltatar.py @@ -486,13 +486,13 @@ class DeltaTar(object): else: comptype = 'tar' - enctype = '' + encver = None if 'aes' in self.index_mode: - enctype = 'aes' + encver = 1 return tarfile._Stream(name=path, mode=mode, comptype=comptype, bufsize=tarfile.RECORDSIZE, fileobj=None, - enctype=enctype, password=self.password) + encver=encver, password=self.password) def create_full_backup(self, source_path, backup_path, max_volume_size=None, extra_data=dict()): diff --git a/deltatar/tarfile.py b/deltatar/tarfile.py index 35c538d..13fb489 100644 --- a/deltatar/tarfile.py +++ b/deltatar/tarfile.py @@ -53,7 +53,6 @@ import copy import re import operator -#from . import aescrypto from . import crypto try: @@ -113,6 +112,9 @@ GNU_FORMAT = 1 # GNU tar format PAX_FORMAT = 2 # POSIX.1-2001 (pax) format DEFAULT_FORMAT = GNU_FORMAT +DELTATAR_HEADER_VERSION = 1 +DELTATAR_PARAMETER_VERSION = 1 + #--------------------------------------------------------- # tarfile constants #--------------------------------------------------------- @@ -362,7 +364,7 @@ class _Stream: """ def __init__(self, name, mode, comptype, fileobj, bufsize, - concat_stream=False, enctype='', password="", + concat_stream=False, encver=None, password="", compresslevel=9): """Construct a _Stream object. """ @@ -389,7 +391,7 @@ class _Stream: self.flags = 0 self.internal_pos = 0 self.concat_stream = concat_stream - self.enctype = enctype + self.encver = encver self.password = password self.last_block_offset = 0 self.dbuf = b"" @@ -399,9 +401,6 @@ class _Stream: self.bytes_written = 0 try: - if enctype != "" and enctype not in VALID_ENCRYPTION_MODES: - raise InvalidEncryptionError("unsupported encryption mode %r" - % enctype) if comptype == "gz": try: import zlib @@ -409,8 +408,14 @@ class _Stream: raise CompressionError("zlib module is not available") self.zlib = zlib if mode == "r": - if self.enctype == 'aes': - self.encryption = crypto.AES_GCM_context(self.password) + if self.encver is not None: + try: + enc = crypto.Crypto (crypto.DECRYPT, pw, nacl, 1) + except ValueError as exn: + raise InvalidEncryptionError \ + ("ctor failed (%r, , “%s”, %r)" + % (crypto.DECRYPT, nacl, 1)) + self.encryption = enc self._init_read_gz() self.exception = zlib.error else: @@ -418,10 +423,10 @@ class _Stream: self.crc = zlib.crc32(b"") & 0xFFFFffff elif comptype == "bz2": - if self.enctype != "": - raise InvalidEncryptionError("encryption mode %r not " + if self.encver is not None: + raise InvalidEncryptionError("encryption version %r not " "available for compression %s" - % (enctype, comptype)) + % (encver, comptype)) try: import bz2 except ImportError: @@ -434,10 +439,10 @@ class _Stream: self.cmp = bz2.BZ2Compressor() elif comptype == 'xz': - if self.enctype != "": - raise InvalidEncryptionError("encryption mode %r not " + if self.encver is not None: + raise InvalidEncryptionError("encryption version %r not " "available for compression %s" - % (enctype, comptype)) + % (encver, comptype)) try: import lzma except ImportError: @@ -449,17 +454,31 @@ class _Stream: else: self.cmp = lzma.LZMACompressor() - elif self.enctype == 'aes': - self.encryption = aescrypto.AESCrypt(self.password) - if mode != "r": + elif self.encver is not None: + if mode == "r": + try: + enc = crypto.Crypto (crypto.DECRYPT, pw, nacl, 1) + except ValueError as exn: + raise InvalidEncryptionError \ + ("ctor failed (%r, , “%s”, %r)" + % (crypto.DECRYPT, nacl, 1)) + self.encryption = enc + else: + try: + enc = crypto.Crypto (crypto.ENCRYPT, pw, nacl, 1) + except ValueError as exn: + raise InvalidEncryptionError \ + ("ctor failed (%r, , “%s”, %r)" + % (crypto.DECRYPT, nacl, 1)) + self.encryption = enc self.encryption.init() self.__write_to_file(self.encryption.salt_str) elif comptype != "tar": - if self.enctype != "": - raise InvalidEncryptionError("encryption mode %r not " + if self.encver is not None: + raise InvalidEncryptionError("encryption version %r not " "available for compression %s" - % (enctype, comptype)) + % (encver, comptype)) raise CompressionError("unknown compression type %r" % comptype) except: @@ -482,10 +501,14 @@ class _Stream: 0) # if aes, we encrypt after compression - if self.enctype == 'aes': - self.encryption = aescrypto.AESCrypt(self.password) - self.encryption.init() - self.__write_to_file(self.encryption.salt_str) + if self.encver is not None: + hdr = crypto.hdr_from_params \ + (version=DELTATAR_HEADER_VERSION, + paramversion=self.encver, + nacl=self.encryption.salt_str, + iv=self.encryption.iv, + ctsize=self.encryption.ctsize) + self.__write_to_file(hdr) timestamp = struct.pack("