add SCRYPT hashing mode to crypto.py
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Thu, 11 May 2017 08:35:40 +0000 (10:35 +0200)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Mon, 2 Apr 2018 11:34:08 +0000 (13:34 +0200)
Add a subcommand “scrypt” to crypto.py in CLI mode. Example:

    $ python3 ./deltatar/crypto.py scrypt foo -i - -o pwd   \
        <backup_dir/bfull-2017-05-11-0919-001.tar.pdtcrypt
    {"scrypt_params": {"p": 1, "N": 65536, "dkLen": 16, "r": 8},
     "salt": "b'fbdbaa9890ae243eb16391199c9243f6'", "hash":
     "b'1e7d7a78b9300d461779e9c80e4a15ac'"}

The output “hash” is calculated from the salt in the first
header found in the given archive and the password specified.

deltatar/crypto.py

index dab3f82..e4a121a 100755 (executable)
@@ -72,6 +72,7 @@ import os
 import struct
 import sys
 import time
+import types
 try:
     import enum34
 except ImportError as exn:
@@ -178,7 +179,6 @@ ENCRYPTION_PARAMETERS = \
                    , "NaCl_LEN" : 16 })
         , "enc": "aes-gcm" } }
 
-
 ###############################################################################
 ## constants
 ###############################################################################
@@ -425,8 +425,9 @@ def kdf_scrypt (params, password, nacl):
     return SCRYPT_KEY_MEMO [key_parms], nacl
 
 
-def kdf_by_version (paramversion):
-    defs = ENCRYPTION_PARAMETERS.get(paramversion)
+def kdf_by_version (paramversion=None, defs=None):
+    if paramversion is not None:
+        defs = ENCRYPTION_PARAMETERS.get(paramversion, None)
     if defs is None:
         raise ValueError ("no encryption parameters for version %r"
                 % paramversion)
@@ -843,8 +844,16 @@ _testing_set_PDTCRYPT_MAX_OBJ_SIZE = \
 ## freestanding invocation
 ###############################################################################
 
+PDTCRYPT_SUB_PROCESS = 0
+PDTCRYPT_SUB_SCRYPT  = 1
+
+PDTCRYPT_SUB = \
+        { "process" : PDTCRYPT_SUB_PROCESS
+        , "scrypt"  : PDTCRYPT_SUB_SCRYPT }
+
 PDTCRYPT_DECRYPT   = 1 << 0 # decrypt archive with password
 PDTCRYPT_SPLIT     = 1 << 1 # split archive into individual objects
+PDTCRYPT_HASH      = 1 << 2 # output scrypt hash for file and given password
 
 PDTCRYPT_SPLITNAME = "pdtcrypt-object-%d.bin"
 
@@ -1087,16 +1096,92 @@ def depdtcrypt_file (pw, spath, dpath):
             return depdtcrypt (pw, ins, outs)
 
 
+def mode_depdtcrypt (mode, pw, ins, outs):
+    try:
+        total_read, total_obj, total_ct, total_pt = \
+            depdtcrypt (mode, pw, ins, outs)
+    except DecryptionError as exn:
+        noise ("PDT: Decryption failed:")
+        noise ("PDT:")
+        noise ("PDT:    “%s”" % exn)
+        noise ("PDT:")
+        noise ("PDT: Did you specify the correct password?")
+        noise ("")
+        return 1
+    except PDTSplitError as exn:
+        noise ("PDT: Split operation failed:")
+        noise ("PDT:")
+        noise ("PDT:    “%s”" % exn)
+        noise ("PDT:")
+        noise ("PDT: Hint: target directory should to be empty.")
+        noise ("")
+        return 1
+
+    if PDTCRYPT_VERBOSE is True:
+        noise ("PDT: decryption successful"                 )
+        noise ("PDT:   %.10d bytes read"        % total_read)
+        noise ("PDT:   %.10d objects decrypted" % total_obj )
+        noise ("PDT:   %.10d bytes ciphertext"  % total_ct  )
+        noise ("PDT:   %.10d bytes plaintext"   % total_pt  )
+        noise (""                                           )
+
+    return 0
+
+
+def mode_scrypt (pw, ins):
+    hdr = None
+    try:
+        hdr = hdr_read_stream (ins)
+    except EndOfFile as exn:
+        noise ("PDT: malformed input: end of file reading first object header")
+        noise ("PDT:")
+        return 1
+    finally:
+        ins.close ()
+
+    nacl = hdr ["nacl"]
+    pver = hdr ["paramversion"]
+    if PDTCRYPT_VERBOSE is True:
+        noise ("PDT: salt of first object          : %s" % binascii.hexlify (nacl))
+        noise ("PDT: parameter version of archive  : %d" % pver)
+
+    try:
+        defs = ENCRYPTION_PARAMETERS.get(pver, None)
+        kdfname, params = defs ["kdf"]
+        if kdfname != "scrypt":
+            noise ("PDT: input is not an SCRYPT archive")
+            noise ("")
+            return 1
+        kdf = kdf_by_version (None, defs)
+    except ValueError as exn:
+        noise ("PDT: object has unknown parameter version %d" % pver)
+
+    hsh, _void = kdf (pw, nacl)
+
+    import json
+    out = json.dumps ({ "salt"          : str (binascii.hexlify (nacl))
+                      , "hash"          : str (binascii.hexlify (hsh))
+                      , "scrypt_params" : { "N"     : params ["N"]
+                                          , "r"     : params ["r"]
+                                          , "p"     : params ["p"]
+                                          , "dkLen" : params ["dkLen"] } })
+    print (out)
+
+
 def usage (err=False):
     out = print
     if err is True:
         out = noise
-    out ("usage: %s { --help" % SELF)
-    out ("          | [ -v ] { PASSWORD } { { -i | --in }  { - | SOURCE } }")
-    out ("                                { { -o | --out } { - | DESTINATION } }")
-    out ("                                { -D | --no-decrypt } { -S | --split }")
+    out ("usage: %s SUBCOMMAND { --help" % SELF)
+    out ("                     | [ -v ] { PASSWORD } { { -i | --in }  { - | SOURCE } }")
+    out ("                                           { { -o | --out } { - | DESTINATION } }")
+    out ("                                           { -D | --no-decrypt } { -S | --split }")
     out ("")
     out ("\twhere")
+    out ("\t\tSUBCOMMAND      main mode: { process | scrypt }")
+    out ("\t\t                where:")
+    out ("\t\t                   process: extract objects from PDT archive")
+    out ("\t\t                   scrypt:  calculate hash from password and first object")
     out ("\t\tPASSWORD        password to derive the encryption key from")
     out ("\t\t-s              enforce strict handling of initialization vectors")
     out ("\t\t-i SOURCE       file name to read from")
@@ -1121,6 +1206,31 @@ def parse_argv (argv):
     argvi = iter (argv)
     SELF  = os.path.basename (next (argvi))
 
+    try:
+        rawsubcmd  = next (argvi)
+        subcommand = PDTCRYPT_SUB [rawsubcmd]
+    except StopIteration:
+        noise ("ERROR: subcommand required")
+        noise ("")
+        usage (err=True)
+        raise Unreachable
+    except KeyError:
+        noise ("ERROR: invalid subcommand “%s” specified" % rawsubcmd)
+        noise ("")
+        usage (err=True)
+        raise Unreachable
+
+    def checked_pw (arg):
+        nonlocal pw
+        if pw is None:
+            pw = arg
+        else:
+            noise ("ERROR: unqualified argument “%s” but password already "
+                "given" % arg)
+            noise ("")
+            usage (err=True)
+            raise Unreachable
+
     for arg in argvi:
         if arg in [ "-h", "--help" ]:
             usage ()
@@ -1128,43 +1238,54 @@ def parse_argv (argv):
         elif arg in [ "-v", "--verbose", "--wtf" ]:
             global PDTCRYPT_VERBOSE
             PDTCRYPT_VERBOSE = True
-        elif arg in [ "-s", "--strict-ivs" ]:
-            global PDTCRYPT_STRICTIVS
-            PDTCRYPT_STRICTIVS = True
         elif arg in [ "-i", "--in", "--source" ]:
             insspec = next (argvi)
             if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt from %s" % insspec)
         elif arg in [ "-o", "--out", "--dest", "--sink" ]:
             outsspec = next (argvi)
             if PDTCRYPT_VERBOSE is True: noise ("PDT: decrypt to %s" % outsspec)
-        elif arg in [ "-f", "--force" ]:
-            global PDTCRYPT_OVERWRITE
-            PDTCRYPT_OVERWRITE = True
-            if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
-        elif arg in [ "-S", "--split" ]:
-            mode |= PDTCRYPT_SPLIT
-            if PDTCRYPT_VERBOSE is True: noise ("PDT: split files")
-        elif arg in [ "-D", "--no-decrypt" ]:
-            mode &= ~PDTCRYPT_DECRYPT
-            if PDTCRYPT_VERBOSE is True: noise ("PDT: not decrypting")
         else:
-            if pw is None:
-                pw = arg
+            if subcommand == PDTCRYPT_SUB_PROCESS:
+                if arg in [ "-s", "--strict-ivs" ]:
+                    global PDTCRYPT_STRICTIVS
+                    PDTCRYPT_STRICTIVS = True
+                elif arg in [ "-f", "--force" ]:
+                    global PDTCRYPT_OVERWRITE
+                    PDTCRYPT_OVERWRITE = True
+                    if PDTCRYPT_VERBOSE is True: noise ("PDT: overwrite existing files")
+                elif arg in [ "-S", "--split" ]:
+                    mode |= PDTCRYPT_SPLIT
+                    if PDTCRYPT_VERBOSE is True: noise ("PDT: split files")
+                elif arg in [ "-D", "--no-decrypt" ]:
+                    mode &= ~PDTCRYPT_DECRYPT
+                    if PDTCRYPT_VERBOSE is True: noise ("PDT: not decrypting")
+                else:
+                    checked_pw (arg)
+            elif subcommand == PDTCRYPT_SUB_SCRYPT:
+                checked_pw (arg)
             else:
-                noise ("ERROR: unqualified argument “%s” but password already "
-                       "given" % arg)
-                noise ("")
-                usage (err=True)
                 raise Unreachable
 
-    if mode & PDTCRYPT_DECRYPT and pw is None:
-        noise ("ERROR: encryption requested but no password given")
-        noise ("")
-        usage (err=True)
-        raise Unreachable
+    if pw is not None:
+        pw = pw.encode ()
+    else:
+        if subcommand == PDTCRYPT_SUB_SCRYPT:
+            noise ("ERROR: scrypt hash mode requested but no password given")
+            noise ("")
+            usage (err=True)
+            raise Unreachable
+        elif mode & PDTCRYPT_DECRYPT:
+            noise ("ERROR: encryption requested but no password given")
+            noise ("")
+            usage (err=True)
+            raise Unreachable
 
     # default to stdout
     ins  = deptdcrypt_mk_stream (PDTCRYPT_SOURCE, insspec  or "-")
+
+    if subcommand == PDTCRYPT_SUB_SCRYPT:
+        return True, partial (mode_scrypt, pw, ins)
+
     if mode & PDTCRYPT_SPLIT: # destination must be directory
         if outsspec is None or outsspec == "-":
             noise ("ERROR: split mode is incompatible with stdout sink")
@@ -1190,42 +1311,19 @@ def parse_argv (argv):
             noise ("")
             usage (err=True)
             raise Unreachable
+
     else:
         outs = deptdcrypt_mk_stream (PDTCRYPT_SINK, outsspec or "-")
-    return mode, pw, ins, outs
+
+    return True, partial (mode_depdtcrypt, mode, pw, ins, outs)
 
 
 def main (argv):
-    mode, pw, ins, outs  = parse_argv (argv)
-    try:
-        total_read, total_obj, total_ct, total_pt = \
-            depdtcrypt (mode, pw, ins, outs)
-    except DecryptionError as exn:
-        noise ("PDT: Decryption failed:")
-        noise ("PDT:")
-        noise ("PDT:    “%s”" % exn)
-        noise ("PDT:")
-        noise ("PDT: Did you specify the correct password?")
-        noise ("")
-        return 1
-    except PDTSplitError as exn:
-        noise ("PDT: Split operation failed:")
-        noise ("PDT:")
-        noise ("PDT:    “%s”" % exn)
-        noise ("PDT:")
-        noise ("PDT: Hint: target directory should to be empty.")
-        noise ("")
-        return 1
+    ok, runner = parse_argv (argv)
 
-    if PDTCRYPT_VERBOSE is True:
-        noise ("PDT: decryption successful"                 )
-        noise ("PDT:   %.10d bytes read"        % total_read)
-        noise ("PDT:   %.10d objects decrypted" % total_obj )
-        noise ("PDT:   %.10d bytes ciphertext"  % total_ct  )
-        noise ("PDT:   %.10d bytes plaintext"   % total_pt  )
-        noise (""                                           )
+    if ok is True: return runner ()
 
-    return 0
+    return 1
 
 
 if __name__ == "__main__":