diff options
author | mjfernez <mjfernez@gmail.com> | 2021-03-06 16:36:31 -0500 |
---|---|---|
committer | mjfernez <mjfernez@gmail.com> | 2021-03-06 16:36:31 -0500 |
commit | 9ae2789aa268b37716b5296263497c1aa35cf590 (patch) | |
tree | 639acf2a5ed922e26db2153b5ca83ede941cfb5c /fhash | |
download | scripts-n-tools-9ae2789aa268b37716b5296263497c1aa35cf590.tar.gz |
first commit
Diffstat (limited to 'fhash')
-rw-r--r-- | fhash/LICENSE | 21 | ||||
-rw-r--r-- | fhash/README.md | 84 | ||||
-rw-r--r-- | fhash/fhash-example.png | bin | 0 -> 53702 bytes | |||
-rwxr-xr-x | fhash/fhash.py | 285 | ||||
-rw-r--r-- | fhash/lolcat.png | bin | 0 -> 11877 bytes | |||
-rw-r--r-- | fhash/tests/basic/TESTING | 4 | ||||
-rw-r--r-- | fhash/tests/basic/smolgrownup.png | bin | 0 -> 1984243 bytes | |||
-rw-r--r-- | fhash/tests/basic/textfile | 1 | ||||
-rw-r--r-- | fhash/tests/shattered/TESTING | 10 | ||||
-rw-r--r-- | fhash/tests/shattered/shattered-1.pdf | bin | 0 -> 422435 bytes | |||
-rw-r--r-- | fhash/tests/shattered/shattered-2.pdf | bin | 0 -> 422435 bytes |
11 files changed, 405 insertions, 0 deletions
diff --git a/fhash/LICENSE b/fhash/LICENSE new file mode 100644 index 0000000..0244df3 --- /dev/null +++ b/fhash/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Michael Fernez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/fhash/README.md b/fhash/README.md new file mode 100644 index 0000000..a0b5877 --- /dev/null +++ b/fhash/README.md @@ -0,0 +1,84 @@ +# fhash + +## A simple python tool for generating, passing and formatting hash values + +So some may not agree, but I find hash values make very good master passwords for tools like +password managers and other security software. Why go with just a passphrase when you can pipe +the sucker in a few one-way functions to scramble it? I've found this is only a practical +solution though if: +1) You know you will always be able to copy and paste on the device you are using +2) You know you will always have a hashing tool either natively or available online + +For convenience, I used online tools for years, but this led to bad habits. See some tools like +to use caps (it is a hex number after all) but some stick with lowercase. Seems like a useless +distinction and it probably is, but since some of my hash passwords are upper case and some are +lower, it'd be nice to have a simple portable tool to do that so I don't need to rely on some +random guy on the internet. You could use openssl of course, but that's giving you a lot more +than you need. And other command line hashing functions usually stick to one algorithm like +md5sum. + +For example, here's how you'd take the sha3 hash, with a message size of 256, and capitalize it in bash: + +`echo "password" | openssl dgst -sha3-256 | cut -c 10- | tr "[:lower:]" "[:upper:]"` + +Not awful, but a little cumbersome + +So this is my attempt to make a tool in the middle of those. My main goal for this tool is that +it can serve as an educational resource for those learning how to make command line tools with +Python and how to use hash functions. And for making it easy to pipe hashes to other programs. + +All libraries are in the Python Standard Library, all you need is Python 3, no other dependencies! + +The script is self contained and executable so Linux users can just use: + +`$ ./fhash.py sha2 -i "Hello, World!"` + +Or if you don't like the .py on the end you can rename it with: + +`$ mv fhash.py fhash` + +If you really like it and want to use it from anywhere on your system, copy it into your path (run as root): + +`# cp fhash.py /usr/local/bin/fhash.py` + +The program also runs on Windows, but you'll need to run it through python: + +`> python3 fhash.py sha2 -i "I<3Windows"` + +The default output can be passed to other commands as in: + +`$ fhash.py md5 -i $(fhash.py sha2 -i "Hello, world\!")` + +Or you can pipe the output like this: + +`$ fhash.py sha2 -i "Hello, world\!" -f uppercase | fhash.py md5 -i {}` + +Or if you want to be fancy, pipe to other formatting programs :) + +`$ fhash.py sha3 -i "LOLcats\!" -f uppercase | lolcat` + +![alt text](./lolcat.png?raw=True) + +Or go completely crazy: + +![alt text](./fhash-example.png?raw=True) + +The input need not be a string, you can also put files, a list of files, or a +folder to hash. You can also use descriptors like '\*' or '.' + +For SHA2 and SHA3, you can set the size of the output using the '-s' flag. +Run with '-s 0' to see a list of supported sizes.<br> +This flag is simply ignored for SHA1 and MD5 since they only have one output size + +You can optionally use the '-v' flag for more details and timing, but you will not be able to +pipe the output the same way, so this is more of an experimental option than anything + +Use '-h' '--help' to see the list of flags and how to use them. If you have suggestions on how +to improve the help menu to make the tool easier to understand, please submit a pull request or +open an issue! + +Don't run the script itself as administrator by the way, this sometimes messes things up when opening files + + +Any and all contributors welcome. Please just make sure any edits are compliant with PEP8.<br> +I highly recommend this tool for editing: https://github.com/coala/coala diff --git a/fhash/fhash-example.png b/fhash/fhash-example.png Binary files differnew file mode 100644 index 0000000..70efad8 --- /dev/null +++ b/fhash/fhash-example.png diff --git a/fhash/fhash.py b/fhash/fhash.py new file mode 100755 index 0000000..4576873 --- /dev/null +++ b/fhash/fhash.py @@ -0,0 +1,285 @@ +#!/usr/bin/python3 +""" +fhash is a simple python tool for generating, passing and formatting hash +values. It can be used to print hashes of strings or files in various formats +including as a hex string (lower and uppercase), binary string, or decimal +number. It can also be used for testing the speed of these various functions + +""" +# OS/filesystem +import os +import sys +import locale + +# Hash functions +import hashlib + +# CLI tool +import argparse + +#IO and time +import time + +# Defines the basic options and constants used +algos = ['md5', 'sha1', 'sha2', 'sha3'] +modes = [224, 256, 384, 512] +formats = ['lowercase', 'uppercase', 'binary', 'decimal'] +os_encoding = locale.getpreferredencoding() + +# For sha256 and sha3 only +DEFAULT_SIZE = 256 + +# For the verbose option, enables warning and error messages +VERBOSE = False + +# For splitting data into chunks for larger files for accurate hash values +BUFF_SIZE = 2048 + +# For handling directories +DIR_ERROR = 'Error: "{}" specified is a directory.' + + +def check_function(func, size): + """Specifies the size in bits of the chosen function. + If the user put in a invalid length, throw an error + Returns the default length if no size is provided + @func: the hash function use (md5, sha1, sha2, sha3) + @size: the desired length of the output. i.e. sha256 + outputs a message of 256 bits or 64 hex-digits + """ + if (func in algos[0:2]): + bsize = 160 if (algos.index(func)) else 128 + if (size == None): + v_print('Using default message size of \ + {} bits'.format(bsize)) + else: + v_print('Warning: "size" option is ignored for \ + sha1 and md5 since they are fixed-length') + v_print('Using message size of {} bits'.format(bsize)) + return bsize + else: + if (size == None): + v_print('Using default message size, 256-bits') + return DEFAULT_SIZE + elif (size not in modes): + print('Error: Message size must be \ + 224, 256, 384, or 512 for {}'.format(func)) + sys.exit() + else: + v_print('Using message size of {} bits'.format(size)) + return size + + +def format_output(msg, fmt): + """Formats the hash value according to the user's choice + @msg: the string to be formatted + @fmt: the target format + """ + if (fmt == 'lowercase'): + return msg.lower() + elif (fmt == 'uppercase'): + return msg.upper() + elif (fmt == 'binary'): + # Omit the trailing 0b + return '{}'.format(bin(int(msg, 16))[2:].zfill(8)) + elif (fmt == 'decimal'): + return str(int(msg, 16)) + else: + print('You somehow passed an invalid format. Please file a bug report') + sys.exit() + + +def get_algorithm(func, size): + """Returns the requested hash function "func" at the specified "size" + (if it applies) + @func: the hash function use (md5, sha1, sha2, sha3) + @size: the desired length of the output. i.e. sha256 + outputs a message of 256 bits or 64 hex-digits + """ + if (func == 'md5'): + return hashlib.md5() + elif(func == 'sha1'): + return hashlib.sha1() + elif (func == 'sha2'): + return eval('hashlib.sha' + str(size) + '()') + elif (func == 'sha3'): + return eval('hashlib.sha3_' + str(size) + '()') + else: + print('You somehow passed an invalid function, \ + which should not happen.' + 'Please file a bug report at https://github.com/mjfernez/fhash') + print('Quitting...') + sys.exit() + + +def get_hash(msg, algo): + """The core hasing function. + This takes an input string or file "msg" and hashes it with "algo" + @msg: the string or file to be hashed + @algo: the hashing algorithm to use + """ + hasher = algo + if (os.path.isfile(msg)): + with open(msg, 'rb') as f: + while True: + buf = f.read(BUFF_SIZE) + + # If there's no data read into the buffer, EOF + if not buf: + break + hasher.update(buf) + else: + buf = msg + hasher.update(buf.encode(os_encoding)) + return hasher.hexdigest() + + +def get_options(args=sys.argv[1:]): + """Sets up the tool to parse arguments correctly + (help menu is added by default) + @args: all arguments inputted by the user + """ + parser = argparse.ArgumentParser() + + parser.add_argument('-i', '--input', + nargs='+', + required=True, + help='Input file or text. \ + Try using sha2 -i "Hello, World!"' + ) + parser.add_argument('-o', '--output', + nargs='?', + default=None, + help='Destination file to save to. ' + 'Prints to screen if none specified. ' + 'You can save a comma separated list ' + 'of files and hashes by specifying the ' + 'file extension ".csv", otherwise saves hash values \ + only as text') + parser.add_argument('-s', '--size', + default=None, + type=int, + help='Message length in bytes 224, 256, 384, 512 ' + '(only valid for sha 2 and 3).') + parser.add_argument('-f', '--format', + choices=formats, + default=formats[0], + help='Formatting for the output') + parser.add_argument('-v', '--verbose', + action='store_true', + help='Optionally add additional information to the \ + output. Without this flag, the program will just \ + print the hash') + parser.add_argument('function', + choices=algos, + help='Use the specified hash function') + + return parser.parse_args(args) + + +def save_output(hashes, output): + """Saves one or more hashes to file. Overwrites the file if it exists + @hashes: the hash or list of hashes to be saved + @output: the output destination specified by the user + (expects a file or "None" to print to screen) + """ + while (os.path.isfile(output)): + opt = input('That file exists. Ok to overwrite? (y/n): ') + if (opt.lower() in ['y', 'yes']): + break + elif (opt.lower() in ['n', 'no']): + output = input('Ok, type the new file name or file path: ') + else: + pass + + with open(output, 'w+') as f: + f.seek(0) + for h in hashes: + f.write(h + '\n') + f.truncate() + print('File {} created!'.format(output)) + + +# v_print is a function conditionally defined if the user passes the verbose +# option. +# This is preferable to writing "if (VERBOSE)" everytime we need to print error +# info +v_print = None + + +def main(): + start = time.perf_counter() + opts = get_options(sys.argv[1:]) + inp = opts.input + out = opts.output + func = opts.function + size = opts.size + fmt = opts.format + VERBOSE = opts.verbose + + if (inp == None): + print('Error: No input given') + sys.exit() + + if VERBOSE: + def _v_print(arg): + print(arg) + else: + _v_print = lambda *a: None # do nothing + + global v_print + v_print = _v_print + + # Check to make sure size and function inputs are valid + size = check_function(func, size) + + # If the user chose to output to a file/directory + if (out != None): + if os.path.isdir(out): + print(DIR_ERROR.format(out) + ' Output must be a file.') + sys.exit() + + # Go through the list of arguments provided by the user after the -i option + # Remove directories + for i in inp: + if os.path.isdir(i): + print(DIR_ERROR.format(i)) + inp.remove(i) + + # After going through the list, check to see if there are any files to hash + # Really not the most elegant way to do this... consider refactoring + if (inp == None): + print('Error: No files in input!') + sys.exit() + + hashes = [] + last_clock = time.perf_counter_ns() + for j in inp: + v_print('Calculating {} hash for "{}"...'.format(func, j)) + md = get_hash(j, get_algorithm(func, size)) + formatted = format_output(md, fmt) + hashes.append(formatted) + if(out == None): + print(formatted) + + curr_clock = time.perf_counter_ns() + v_print('Completed in {}ns'.format(curr_clock - last_clock)) + last_clock = curr_clock + + if (out != None): + if(out.endswith('.csv')): + save_output(['{}, {}'.format(i, j) + for i, j in zip(inp, hashes)], out) + else: + save_output(hashes, out) + + end = time.perf_counter() + v_print('The job took {}s total'.format(end - start)) + + +if __name__ == '__main__': + try: + main() + except (KeyboardInterrupt): + print('\nUser stopped the program') + sys.exit() diff --git a/fhash/lolcat.png b/fhash/lolcat.png Binary files differnew file mode 100644 index 0000000..3c2680d --- /dev/null +++ b/fhash/lolcat.png diff --git a/fhash/tests/basic/TESTING b/fhash/tests/basic/TESTING new file mode 100644 index 0000000..bc1d926 --- /dev/null +++ b/fhash/tests/basic/TESTING @@ -0,0 +1,4 @@ +Per openssl the sha1 hashes should be: + +smolgrownup.png: a7032fda513d3ad28d083599fa227088b2f66062 +textfile: a4a05cfdc38bf07fcaeb1f7de1bc8ae8e783c373 diff --git a/fhash/tests/basic/smolgrownup.png b/fhash/tests/basic/smolgrownup.png Binary files differnew file mode 100644 index 0000000..328a97c --- /dev/null +++ b/fhash/tests/basic/smolgrownup.png diff --git a/fhash/tests/basic/textfile b/fhash/tests/basic/textfile new file mode 100644 index 0000000..f8c4bf1 --- /dev/null +++ b/fhash/tests/basic/textfile @@ -0,0 +1 @@ +This file has text diff --git a/fhash/tests/shattered/TESTING b/fhash/tests/shattered/TESTING new file mode 100644 index 0000000..bddd76e --- /dev/null +++ b/fhash/tests/shattered/TESTING @@ -0,0 +1,10 @@ +These files should yield the same sha1 hash value + +To test run + +`fhash.py sha1 -i shattered-1.pdf` +`fhash.py sha1 -i shattered-2.pdf` + +You should get: 38762cf7f55934b34d179ae6a4c80cadccbb7f0a + +From: http://shattered.io/ diff --git a/fhash/tests/shattered/shattered-1.pdf b/fhash/tests/shattered/shattered-1.pdf Binary files differnew file mode 100644 index 0000000..ba9aaa1 --- /dev/null +++ b/fhash/tests/shattered/shattered-1.pdf diff --git a/fhash/tests/shattered/shattered-2.pdf b/fhash/tests/shattered/shattered-2.pdf Binary files differnew file mode 100644 index 0000000..b621eec --- /dev/null +++ b/fhash/tests/shattered/shattered-2.pdf |