--- /dev/null
+#!/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 = "<H"
+FMT_UINT64_LE = "<Q"
+
+PASSPHRASE = b"test1234"
+AES_GCM_AAD = b"authenticated plain text"
+
+# aes+gcm
+AES_GCM_IV_LEN = 12
+AES_GCM_MAX_SIZE = (1 << 36) - (1 << 5) # 2^39 - 2^8 b ≅ 64 GB
+
+# scrypt
+SCRYPT_dkLen = 16
+SCRYPT_N = 1 << 15
+SCRYPT_r = 8
+SCRYPT_p = 1
+SCRYPT_NaCl_LEN = 16
+
+###############################################################################
+## header
+###############################################################################
+#
+# Interface:
+#
+# struct hdrinfo
+# { version : u16
+# , paramversion : u16
+# , nacl : [u8; 16]
+# , iv : [u8; 12]
+# , ctsize : usize
+# , tag : [u8; 16] }
+# fn hdr_read (f : handle) -> hdrinfo;
+# fn hdr_write (f : handle, h : hdrinfo) -> IOResult<usize>;
+# 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))
+