init crypto support v2
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Thu, 23 Feb 2017 15:34:19 +0000 (16:34 +0100)
committerPhilipp Gesang <philipp.gesang@intra2net.com>
Thu, 23 Feb 2017 15:36:05 +0000 (16:36 +0100)
Implements header reading and writing as well as PoC encryption
wrappers.

WIP

deltatar/crypto.py [new file with mode: 0755]

diff --git a/deltatar/crypto.py b/deltatar/crypto.py
new file mode 100755 (executable)
index 0000000..cbd48f4
--- /dev/null
@@ -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 = "<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))
+