From: Philipp Gesang Date: Thu, 23 Feb 2017 15:34:19 +0000 (+0100) Subject: init crypto support v2 X-Git-Url: http://developer.intra2net.com/git/?a=commitdiff_plain;h=a513b9d9695717937bf5596d537ee142067816c4;p=python-delta-tar init crypto support v2 Implements header reading and writing as well as PoC encryption wrappers. WIP --- diff --git a/deltatar/crypto.py b/deltatar/crypto.py new file mode 100755 index 0000000..cbd48f4 --- /dev/null +++ b/deltatar/crypto.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 + +""" + +=============================================================================== + crypto -- Encryption Layer for the Intra2net Backup +=============================================================================== + +Crypto stack: + + - AES-GCM for the symmetric encryption; + - Scrypt as KDF. + +References: + + - NIST Recommendation for Block Cipher Modes of Operation: Galois/Counter + Mode (GCM) and GMAC + http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf + + - AES-GCM v1: + https://cryptome.org/2014/01/aes-gcm-v1.pdf + + - Authentication weaknesses in GCM + http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/comments/CWC-GCM/Ferguson2.pdf + +""" + +import binascii +import ctypes +import io +import os +import struct +import sys +import time +try: + import enum34 +except ImportError as exn: + pass + +if __name__ == "__main__": ## Work around the import mechanism’s lest Python’s + pwd = os.getcwd() ## preference for local imports causes a cyclical + ## import (crypto → pylibscrypt → […] → ./tarfile → crypto). + sys.path = [ p for p in sys.path if p.find ("deltatar") < 0 ] + +import pylibscrypt +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + + +__all__ = [ "aesgcm_enc", "aesgcm_dec" ] + +############################################################################### +## constants +############################################################################### + +I2N_HDR_MAGIC = b"PDTCRYPT" + +I2N_HDR_SIZE_MAGIC = 8 +I2N_HDR_SIZE_VERSION = 2 +I2N_HDR_SIZE_PARAMVERSION = 2 +I2N_HDR_SIZE_NACL = 16 +I2N_HDR_SIZE_IV = 12 +I2N_HDR_SIZE_CTSIZE = 8 +I2N_HDR_SIZE_TAG = 16 + +I2N_HDR_SIZE = I2N_HDR_SIZE_MAGIC + I2N_HDR_SIZE_VERSION \ + + I2N_HDR_SIZE_PARAMVERSION + I2N_HDR_SIZE_NACL \ + + I2N_HDR_SIZE_IV + I2N_HDR_SIZE_CTSIZE \ + + I2N_HDR_SIZE_TAG # = 64 + +# precalculate offsets since Python can’t do constant folding over names +HDR_OFF_VERSION = I2N_HDR_SIZE_MAGIC +HDR_OFF_PARAMVERSION = HDR_OFF_VERSION + I2N_HDR_SIZE_VERSION +HDR_OFF_NACL = HDR_OFF_PARAMVERSION + I2N_HDR_SIZE_PARAMVERSION +HDR_OFF_IV = HDR_OFF_NACL + I2N_HDR_SIZE_NACL +HDR_OFF_CTSIZE = HDR_OFF_IV + I2N_HDR_SIZE_IV +HDR_OFF_TAG = HDR_OFF_CTSIZE + I2N_HDR_SIZE_CTSIZE + +FMT_UINT16_LE = " hdrinfo; +# fn hdr_write (f : handle, h : hdrinfo) -> IOResult; +# fn hdr_fmt (h : hdrinfo) -> String; +# + +def hdr_read (fin): + if isinstance (fin, int): + try: + fin = os.fdopen (fin) + except OSError as exn: # probably EBADF + return False, "error converting fd %d to io for reading: %r" \ + % (fin, exn) + try: + hdr = fin.read(I2N_HDR_SIZE) + except OSError as exn: + return False, "error reading %d B from %r: %r" \ + % (I2N_HDR_SIZE, fin, exn) + + mag = hdr[:I2N_HDR_SIZE_MAGIC] + if mag != I2N_HDR_MAGIC: + return False, "error reading header from %r: expected %s, got %r" \ + % (fin, I2N_HDR_MAGIC, mag) + + version = hdr[HDR_OFF_VERSION : HDR_OFF_PARAMVERSION] + paramversion = hdr[HDR_OFF_PARAMVERSION : HDR_OFF_NACL] + nacl = hdr[HDR_OFF_NACL : HDR_OFF_IV] + iv = hdr[HDR_OFF_IV : HDR_OFF_CTSIZE] + ctsize = hdr[HDR_OFF_CTSIZE : HDR_OFF_TAG] + tag = hdr[HDR_OFF_TAG : I2N_HDR_SIZE] + + version = struct.unpack (FMT_UINT16_LE, version) + paramversion = struct.unpack (FMT_UINT16_LE, paramversion) + ctsize = struct.unpack (FMT_UINT64_LE, ctsize) + + return True, \ + { "version" : version + , "paramversion" : paramversion + , "nacl" : nacl + , "iv" : iv + , "ctsize" : ctsize + , "tag" : tag + } + + +def hdr_write (fout, hdr): + if isinstance (fout, int): + try: + fout = os.fdopen (fout) + except OSError as exn: # probably EBADF + return False, "error converting fd %d to io for writing: %r" \ + % (fout, exn) + version = struct.pack (FMT_UINT16_LE, hdr[ "version"]) + paramversion = struct.pack (FMT_UINT16_LE, hdr["paramversion"]) + ctsize = struct.pack (FMT_UINT64_LE, hdr[ "ctsize"]) + + sum = 0 + try: + def aux (f, v, s): + ret = fout.write (v) + if ret != s: + return False, \ + "error writing header %s to %r: expected %d B, wrote %d B" \ + % (f, fout, s, ret) + nonlocal sum + sum += ret + return True, sum + + s, e = aux ("magic", I2N_HDR_MAGIC, I2N_HDR_SIZE_MAGIC) + if s is False: return False, e + + s, e = aux ("version", version, I2N_HDR_SIZE_VERSION) + if s is False: return False, e + + s, e = aux ("paramversion", paramversion, I2N_HDR_SIZE_PARAMVERSION) + if s is False: return False, e + + s, e = aux ("nacl", hdr["nacl"], I2N_HDR_SIZE_NACL) + if s is False: return False, e + + s, e = aux ("iv", hdr["iv"], I2N_HDR_SIZE_IV) + if s is False: return False, e + + s, e = aux ("ctsize", ctsize, I2N_HDR_SIZE_CTSIZE) + if s is False: return False, e + + s, e = aux ("tag", hdr["tag"], I2N_HDR_SIZE_TAG) + if s is False: return False, e + + except OSError as exn: + return False, "error writing header to %r after %d B written" \ + % (fout, sum) + + if sum != I2N_HDR_SIZE: + return False, "error writing header to %r; wrote %d B total, " \ + "expected %d" % (fout, sum, I2N_HDR_SIZE) + + return True, sum + + +HDR_FMT = "I2n_header { version: %d, paramversion: %d, nacl: %s[%d]\"," \ + " iv: %s[%d], ctsize: %d, tag: %s[%d] }" + +def hdr_fmt (h): + return HDR_FMT % (h["version"], h["paramversion"], + binascii.hexlify (h["nacl"]), len(h["nacl"]), + binascii.hexlify (h["iv"]), len(h["iv"]), + h["ctsize"], + binascii.hexlify (h["tag"]), len(h["tag"])) + + +def hdr_dump (h): + return " ".join ("%.2x%.2x" % (c1, c2) for c1, c2 in zip (h[0::2], h[1::2])) + + +############################################################################### +## {de,en}cryption +############################################################################### + +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, tag) + , backend = default_backend ()) \ + . decryptor () + dec.authenticate_additional_data (aad) + return dec.update (data) + dec.finalize () + +class AES_GCM (object): + """ + Thin wrapper context over AES encryption. + """ + + key = None + data = None + aad = None + + def __init__ (key, data, aad): + self.key = key + self.data = data + self.aad = aad + + +############################################################################### +## keys +############################################################################### + +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) + + +############################################################################### +## freestanding invocation +############################################################################### + +def faux_hdr (): + return \ + { "version" : 42 + , "paramversion" : 2187 + , "nacl" : binascii.unhexlify(b"0011223344556677" + b"8899aabbccddeeff") + , "iv" : binascii.unhexlify(b"0011223344556677" + b"8899aabb") + , "ctsize" : 1337 + , "tag" : binascii.unhexlify(b"ffeeddccbbaa9988" + b"7766554433221100") + } + + +def main (argv): + h = faux_hdr () + print ("before: %s" % hdr_fmt (h)) + c = io.BytesIO () + s, v = hdr_write (c, h) + if s is False: + print ("error writing header [%s] to %r (%s)" + % (hdr_dump (h), s, v), file=sys.stderr) + return -1 + b = c.getvalue() + print ("packed value: %s[%d]" % (hdr_dump (b), len (b))) + s, v = hdr_read (io.BytesIO(b)) + if s is False: + print ("error reading header from %r (%s)" + % (s, v), file=sys.stderr) + return -1 + print ("after: %s" % hdr_fmt (h)) + #NaCl, key = scrypt_derive (PASSPHRASE) + #iv, ciphertext, tag = aesgcm_enc (key, b"plain text", AES_GCM_AAD) + #plaintext = aesgcm_dec (key, ciphertext, AES_GCM_AAD, iv, tag) + return 0 + +if __name__ == "__main__": + sys.exit (main (sys.argv)) +