#!/usr/bin/env python3 """ Given a hash checksum file, validate it's checksums SPDX-License-Identifier: MIT """ import argparse import hashlib import os import signal import sys __version__ = "0.0.1" def parse_args(): """Argument parsing routine""" parser = argparse.ArgumentParser(description="hashcheck") parser.add_argument( "--version", action="version", version=__version__, help="Display the version" ) parser.add_argument( "-c", "--checksum", required=True, dest="checksum", help="Checksum file to read" ) parser.add_argument( "-t", "--type", required=False, dest="type", default="auto", choices=["auto", "md5", "sha1", "sha224", "sha256", "sha384", "sha512"], help=( "Hash type: auto (default), md5, sha1, sha224, " "sha256, sha384, sha512" ), ) return parser.parse_args() def sigbye_handler(signal, frame): """Exit function triggered by caught signals""" sys.exit(0) def guess_type(hstring): """Given an input string, guess the crypto hash based on length""" hexsize = { 32: "md5", 40: "sha1", 56: "sha224", 64: "sha256", 96: "sha384", 128: "sha512", } return hexsize.get(len(hstring.strip()), "NaN") if __name__ == "__main__": """Main entry point for hashcheck""" # register a clean shutdown for the usual signals signal.signal(signal.SIGINT, sigbye_handler) signal.signal(signal.SIGQUIT, sigbye_handler) signal.signal(signal.SIGTERM, sigbye_handler) # parse cmdline arguments args = parse_args() # for each line in the checksum, calculate the hash of the file # and compare it against the digest it's supposed to be try: with open(args.checksum, "r") as csum: # a hash file 99.999% of the time has a relative path of the # file, so we'll attempt to cwd() there to help the user out startdir = os.getcwd() if os.path.dirname(args.checksum): # if already in the directory, dirname() returns '' which is # an exception result when used with chdir() os.chdir(os.path.dirname(args.checksum)) # read our checksum and do the needful for line in csum: line = line.rstrip("\n") # hash checksum = "abcdef file.name" where the second # "space" is actually one of: *, ?, ^, or ' ' (space) hparts = line.split(" ", 1) if len(hparts) != 2: print("Invalid line, skipping.") continue # hparts[0] = hash, hparts[1] = 1char type + name # in auto mode, we can try and guess the type of hash used # by the checksum length, which allows for mixed types of # hashes in one file; otherwise, we have to use what was # indicated on the cli htype = args.type if htype == "auto": htype = guess_type(hparts[0]) if htype == "NaN": print("Unable to guess hash type: {}".format(line)) continue # attempt to instantiate the hashlib.foo() try: hasher = hashlib.new(htype) except AttributeError as error: print("Python hashlib does not support hash: {}".format(args.type)) continue # calculate the hash of the file and compare try: # could be hashin a 4G iso file, calculate in chunks with open(hparts[1][1:], "rb") as hfile: for chunk in iter(lambda: hfile.read(4096), b""): hasher.update(chunk) # simply compare our two values if hasher.hexdigest() == hparts[0]: print("Pass: {}".format(line)) else: print("Fail: {}".format(line)) except (IOError, OSError) as error: # py2 = IOError, py3 = FileNotFound print("Unable to open file: {}".format(hparts[1][1:])) continue # usually python exits and leaves the user where they began, # trust but verify - doesn't hurt to make sure os.chdir(startdir) except (IOError, OSError) as error: # py2 = IOError, py3 = FileNotFound print("Unable to open checksum file: {}".format(args.checksum)) sys.exit(1) sys.exit(0)