, "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
###############################################################################
}
-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 }"
###############################################################################
-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
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)
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))
###############################################################################
import re
import operator
-#from . import aescrypto
from . import crypto
try:
PAX_FORMAT = 2 # POSIX.1-2001 (pax) format
DEFAULT_FORMAT = GNU_FORMAT
+DELTATAR_HEADER_VERSION = 1
+DELTATAR_PARAMETER_VERSION = 1
+
#---------------------------------------------------------
# tarfile constants
#---------------------------------------------------------
"""
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.
"""
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""
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
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, <PASSWORD>, “%s”, %r)"
+ % (crypto.DECRYPT, nacl, 1))
+ self.encryption = enc
self._init_read_gz()
self.exception = zlib.error
else:
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:
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:
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, <PASSWORD>, “%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, <PASSWORD>, “%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:
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("<L", int(time.time()))
self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
raise CompressionError("new compression blocks can only be added in mode 'w'")
if self.comptype == "gz":
self._new_gz_block(True)
- elif self.enctype == 'aes':
+ elif self.encver is not None:
self._new_aes_block(True)
else:
raise CompressionError("Concat compression only available for comptype 'gz'")
0)
# if aes, we encrypt after compression
- if self.enctype == 'aes':
+ if self.encver is not None:
self._new_aes_block(set_last_block_offset)
elif set_last_block_offset:
self.last_block_offset = self.fileobj.tell()
if self.comptype == "tar":
self.close(close_fileobj=False)
self.closed = False
- self.__write_to_file(self.encryption.close_enc())
+
+ data, tag = self.encryption.next (aad=self.name)
+ if data is not None or tag is not None:
+ self.__write_to_file(data)
+ self.__write_to_file(tag)
if set_last_block_offset:
self.last_block_offset = self.fileobj.tell()
- self.encryption = aescrypto.AESCrypt(self.password)
- self.encryption.init()
- self.__write_to_file(self.encryption.salt_str)
+
+ 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)
def write(self, s):
"""Write string s to the stream.
the file
'''
tow = s
- if self.enctype == 'aes':
- tow = self.encryption.encrypt(s)
+ if self.encver is not None:
+ tow = self.encryption.process(s)
self.__write_to_file(tow)
def estim_file_size(self):
result += len(self.buf)
if self.comptype == 'gz':
result += 8 # 2 longs = 8 byte (no extra info written for bzip2)
- if self.enctype == 'aes':
+ if self.encver is not None:
result += self.encryption.bs # (salt was already written at start)
return result
self.__enc_write(chunk)
if close_fileobj and not self._extfileobj:
- if self.enctype == 'aes' and self.mode == "w":
+ if self.encver is not None and self.mode == "w":
self.__write_to_file(self.encryption.close_enc())
self.fileobj.close()
pass
else:
raise ReadError("invalid compressed data")
-
if self.comptype == "gz" and hasattr(self, "crc"):
self.crc = self.zlib.crc32(buf, self.crc) & 0xFFFFffff
if self.concat_stream and len(self.cmp.unused_data) != 0:
Well, actually we not only substract the length of "Salted__", but 16/32
chars because the file is decrypted in multiples of the key size.
'''
- if self.enctype == 'aes':
+ if self.encver is not None:
## XXX
## PHG: this logic doesn’t map to our header-based approach
## and requires adjustment
if not buf: # what else?
return buf ## XXX WTF‽
- idx = buf.find(b'Salted__')
+ idx = buf.find(b'Salted__') ## wat?
if idx == -1:
- return self.encryption.decrypt(buf, last)
+ return self.encryption.process(buf, last) # decrypt
b1 = buf[:idx]
b2 = buf[idx:]
filemode, comptype = mode.split("#", 1)
filemode = filemode or "r"
password = ''
- # if not enctype there's no encryption
- enctype = ''
+ encver = None
if filemode not in "rw":
raise ValueError("mode must be 'r' or 'w'")
- if comptype not in ["gz", "gz.aes128", "gz.aes256", 'aes128',
- 'aes256']:
- raise ValueError("comptype must be 'gz' or 'aes'")
+ # XXX add equivalent check for defined modes
+ #if comptype not in ["gz", "gz.aes128", "gz.aes256", 'aes128',
+ # 'aes256']:
+ # raise ValueError("comptype must be 'gz' or 'aes'")
# encryption gz.aes128 or gz.aes256
if "." in comptype:
- comptype, enctype = comptype.split(".", 1)
- enctype = enctype[:3]
+ comptype, _ = comptype.split(".", 1)
+ encver = 1 # XXX set dynamically
password = kwargs.get('password', '')
if not password:
raise ValueError("you should give a password for encryption")
if comptype.startswith("aes"):
- enctype = comptype
comptype = 'tar'
- enctype = enctype[:3]
+ encver = 1 # XXX set from mode or whatever
password = kwargs.get('password', '')
if not password:
raise ValueError("you should give a password for encryption")
kwargs['concat_compression'] = True
stream = _Stream(name, filemode, comptype, fileobj, bufsize,
- concat_stream=True, enctype=enctype,
+ concat_stream=True, encver=encver,
password=password, compresslevel=compresslevel)
try:
t = cls(name, filemode, stream, **kwargs)
fileobj=None,
bufsize=self.fileobj.bufsize,
password=self.fileobj.password,
- enctype=self.fileobj.enctype,
+ encver=self.fileobj.encver,
concat_stream=self.fileobj.concat_stream)
else:
# here, we lose information about compression/encryption!