aboutsummaryrefslogtreecommitdiffstats
path: root/fhash
diff options
context:
space:
mode:
Diffstat (limited to 'fhash')
-rw-r--r--fhash/LICENSE21
-rw-r--r--fhash/README.md84
-rw-r--r--fhash/fhash-example.pngbin0 -> 53702 bytes
-rwxr-xr-xfhash/fhash.py285
-rw-r--r--fhash/lolcat.pngbin0 -> 11877 bytes
-rw-r--r--fhash/tests/basic/TESTING4
-rw-r--r--fhash/tests/basic/smolgrownup.pngbin0 -> 1984243 bytes
-rw-r--r--fhash/tests/basic/textfile1
-rw-r--r--fhash/tests/shattered/TESTING10
-rw-r--r--fhash/tests/shattered/shattered-1.pdfbin0 -> 422435 bytes
-rw-r--r--fhash/tests/shattered/shattered-2.pdfbin0 -> 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
new file mode 100644
index 0000000..70efad8
--- /dev/null
+++ b/fhash/fhash-example.png
Binary files differ
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
new file mode 100644
index 0000000..3c2680d
--- /dev/null
+++ b/fhash/lolcat.png
Binary files differ
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
new file mode 100644
index 0000000..328a97c
--- /dev/null
+++ b/fhash/tests/basic/smolgrownup.png
Binary files differ
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
new file mode 100644
index 0000000..ba9aaa1
--- /dev/null
+++ b/fhash/tests/shattered/shattered-1.pdf
Binary files differ
diff --git a/fhash/tests/shattered/shattered-2.pdf b/fhash/tests/shattered/shattered-2.pdf
new file mode 100644
index 0000000..b621eec
--- /dev/null
+++ b/fhash/tests/shattered/shattered-2.pdf
Binary files differ