Last active
August 5, 2019 23:04
-
-
Save 0xpizza/6fbba5faf1c8883394b10caf970941ef to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!python3 | |
# coding: utf-8 | |
import hashlib | |
import threading | |
from tkinter import * | |
from tkinter.ttk import * | |
from tkinter import messagebox | |
from tkinter.scrolledtext import ScrolledText | |
import pyperclip | |
__version__ = '0.0.1' | |
__all__ = ['Hasher', 'PasswordGenerator'] | |
AVAILABLE_ALGORITHMS = dict() | |
HASH_LEN = 20 | |
# find and configure available KDF algorithms | |
# if found, add hash function and arguments as a tuple to global list | |
try: | |
import argon2 | |
AVAILABLE_ALGORITHMS.update( | |
argon2 = (argon2.low_level.hash_secret_raw, | |
dict( | |
time_cost=2, | |
memory_cost = 204800, | |
parallelism=8, | |
hash_len=HASH_LEN, | |
type=argon2.Type.ID | |
))) | |
except ImportError: | |
pass | |
try: | |
AVAILABLE_ALGORITHMS.update( | |
scrypt = (hashlib.scrypt, | |
dict( | |
n=2**16, | |
r=8, | |
p=1, | |
dklen=HASH_LEN, | |
maxmem = 0x7fffffff | |
))) | |
except NameError: | |
pass | |
try: | |
AVAILABLE_ALGORITHMS.update( | |
pbkdf2 = (hashlib.pbkdf2_hmac, | |
dict( | |
iterations=300000, | |
dklen=HASH_LEN | |
))) | |
except NameError: | |
pass | |
if len(AVAILABLE_ALGORITHMS) == 0: | |
raise SystemError('Could not find any suitable hashing algorithms!') | |
class ValuedThread(threading.Thread): | |
def __init__(self, *args, **kwargs): | |
self.callback = kwargs.pop('thread_callback', None) | |
super().__init__(*args, **kwargs) | |
self._lock = threading.Lock() | |
self._error = False | |
def run(self): | |
with self._lock: | |
try: | |
r = self._target(*self._args, *self._kwargs) | |
except Exception as e: | |
r = e | |
self._error = True | |
if self.callback is not None: | |
self.callback() | |
self._result = r | |
@property | |
def result(self): | |
with self._lock: | |
return self._result | |
@property | |
def error(self): | |
return self._error | |
class Hasher(): | |
''' | |
Interface function for working with different hash algorithm backends | |
''' | |
@staticmethod | |
def get_available_algorithms(): | |
global AVAILABLE_ALGORITHMS | |
return AVAILABLE_ALGORITHMS | |
def __init__(self, algorithm): | |
a = AVAILABLE_ALGORITHMS[algorithm] | |
self._hash_function = a[0] | |
self._hash_params = a[1] | |
def hash(self, secret, salt): | |
if ( self._hash_function is hashlib.scrypt | |
or self._hash_function is hashlib.pbkdf2_hmac): | |
self._hash_params['salt'] = salt | |
self._hash_params['password'] = secret | |
if self._hash_function is argon2.low_level.hash_secret_raw: | |
self._hash_params['salt'] = salt | |
self._hash_params['secret'] = secret | |
if self._hash_function is hashlib.pbkdf2_hmac: | |
self._hash_params['hash_name'] = 'SHA256' | |
return self._hash_function(**self._hash_params) | |
class PasswordGenerator(): | |
''' | |
This algorithm uses a secure KDF algorithm to compute psuedo random | |
passwords. Initial information provided is combined using the SHA512 | |
algorithm to get a unique salt value which is then used to derive | |
numbers representing the | |
''' | |
def __init__(self, algorithm='argon2', data=None): | |
if algorithm not in AVAILABLE_ALGORITHMS: | |
raise TypeError('hash algorithm %s not supported' % algorithm) | |
else: | |
self._algorithm=algorithm | |
# just a bunch of random bytes from random.org | |
self.public_blob = bytes.fromhex( | |
"2d 40 79 cc 77 e0 f6 d6 3d 9c 4c 75 a0 1f 41 29" | |
"4a 18 a0 d6 dc 2d bb d3 6d f9 de 98 53 c2 46 fd" | |
"9d 42 45 4d 48 c3 fb e3 7f 7f 62 2c 44 f4 4b cf" | |
"cc 92 b5 dd 76 da c9 d9 32 96 10 0f 5c eb c7 4c" | |
) | |
self.char_dict = dict( | |
alphalower = 'abcdefghijklmnopqrstuvwxyz', | |
alphaupper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', | |
numeric = '1234567890', | |
symbol1 = '!@#$/%?^&*()-_=+[]{}<>', | |
symbol2 = "`~,.\\;:'\"", | |
) | |
self.charset = ''.join(s for s in self.char_dict.values()) | |
if data is None: | |
self.data = [] | |
elif isinstance(data, dict): | |
self.data=data | |
def set_algorithm(self, algorithm) : | |
self.__init__(algorithm, data=self.data) | |
def make_password(self): | |
''' | |
adds the data together into a nice hash, passes hash as a | |
salt to the KDF interface, and translates the resulting | |
bytes into a usable password. | |
''' | |
salt = hashlib.sha512() | |
if len(self.data) == 0: | |
raise ValueError('No data supplied') | |
if not all(self.data): | |
raise ValueError('Invalid data supplied') | |
for value in self.data: | |
value = bytes(value, 'utf-8') | |
salt.update(value) | |
salt = salt.digest() | |
hasher = Hasher(self.algorithm) | |
hash = hasher.hash(self.public_blob, salt) | |
# hash represents indices, which we can map to our charset | |
password = '' | |
l = len(self.charset) | |
for i in hash: | |
password += self.charset[i%l] | |
return password | |
@property | |
def algorithm(self): | |
return self._algorithm | |
@property | |
def available_algorithms(self): | |
return list(AVAILABLE_ALGORITHMS.keys()) | |
# coming soon! | |
class StyleChanger(Frame): | |
''' | |
Allow the user to choose between the various ugly ttk themes. | |
Sorry user, there's not much to do about fixing the ugly. | |
''' | |
class CredentialEntry(Entry): | |
''' | |
Special Entry class that hides text when it loses focus. | |
''' | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.pw_char = b'\xe2\x80\xa2'.decode() | |
self.bind("<FocusIn>", self.show_text) | |
self.bind("<FocusOut>", self.hide_text) | |
def hide_text(self, event): | |
self.config(show=self.pw_char) | |
def show_text(self, event): | |
self.config(show='') | |
class PasswordLabel(Label): | |
''' | |
This widget hides your passwords until you click them! | |
''' | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.pw_char = b'\xe2\x80\xa2'.decode() | |
self.bind('<Button-1>', self.show_text) | |
self.bind('<ButtonRelease-1>', self.hide_text) | |
def hide_text(self, event=None): | |
if self.cget("text"): | |
self.config(style="BB.TLabel") | |
def show_text(self, event=None): | |
self.config(style="WB.TLabel") | |
class About(Frame): | |
''' | |
This is a simple "about" page that reads the about.txt file and | |
writes it on the screen. When the user clicks the confirmation | |
button, an indication file is created on the disk. | |
''' | |
def __init__(self, *args, **kwargs): | |
self.callback = kwargs.pop('callback') | |
super().__init__(*args, **kwargs) | |
txt = ScrolledText(self, height=1, width=1, wrap=WORD) | |
btn = Button(self, text='Confirm', state=DISABLED, command=self.accept) | |
txt.pack(fill=BOTH, expand=True, padx=40, pady=5) | |
btn.pack(anchor='center', pady=6) | |
with open('about.txt') as f: | |
txt.insert(1.0, f.read()) | |
txt.configure(state=DISABLED) | |
try: | |
with open('confirmed') as f: | |
btn.configure(state=NORMAL) | |
except FileNotFoundError: | |
self.after(3000, lambda: btn.configure(state=NORMAL)) | |
def accept(self): | |
# creates the file | |
open('confirmed', 'w').close() | |
self.callback() | |
class InputInfo(Frame): | |
def __init__(self, *args, **kwargs): | |
self.about = kwargs.pop('about') | |
super().__init__(*args, **kwargs) | |
self.secret_hash = b'' | |
about_button = Label(self, text="About", style="GF.TLabel") | |
self.pw = PasswordGenerator() | |
# using a list instead of a dict to guarantee item order | |
self._entry_refs = [] # this list corresponds with self.fields | |
self.fields = [ | |
'name', | |
'email', | |
'username', | |
'password', | |
'service', | |
] | |
self.init_body() | |
about_button.bind('<Button-1>', lambda e: self.about()) | |
about_button.pack(anchor=W) | |
def _new_entry(self, parent): | |
''' | |
This method is used to create the text boxes that the user will | |
enter the password entropy data into. The data itself is decoupled | |
into stringvar objects that are kept in a separate list. | |
''' | |
s = StringVar() | |
c = CredentialEntry(parent, textvariable=s, width=40) | |
self._entry_refs.append(s) | |
return c | |
def init_body(self): | |
''' | |
this function builds the body of the object. | |
it should only be called once. | |
''' | |
f = Frame(self) | |
f.pack(pady=5, anchor=E) # anonymous frame to hold hash selector and label | |
Label(f, text='Hash function: ').pack(side=LEFT) | |
algorithms = self.pw.available_algorithms | |
self.algorithm_menu = Combobox(f, values=algorithms) | |
self.algorithm_menu.pack(side=LEFT, padx=6) | |
self.algorithm_menu.set(algorithms[0]) | |
self._prev_algo = algorithms[0] | |
self.algorithm_menu.bind('<<ComboboxSelected>>', self.confirm_algo_change) | |
Frame(self).pack(pady=5) # spacer | |
entry_frame = Frame(self) | |
entry_frame.pack(fill=Y) | |
for i, field in enumerate(self.fields): | |
Label(entry_frame, text=field.title() + ':')\ | |
.grid(row=i, column=0, sticky=W, pady=4) | |
c = self._new_entry(entry_frame) | |
c.grid(row=i, column=1, sticky=E) | |
if i == 0: | |
c.focus_set() | |
Button(self, text='Get Password', command=self.make_password).pack() | |
# this will be used to show a password | |
self.password_area = PasswordLabel(self, text='') | |
self.password_area.pack(expand=True) | |
self.password_area.bind('<Button-3>', self.copy_to_clipboard) | |
# This is used to show information about the user's actions | |
self.message_area = Label(self) | |
self.message_area.pack(expand=True) | |
def copy_to_clipboard(self, event=None): | |
pw = self.password_area.cget("text") | |
pyperclip.copy(pw) | |
self.message_area.config(style='M.TLabel', text='Copied to clipboard') | |
def confirm_algo_change(self, event=None): | |
algo = self.algorithm_menu.get() | |
if algo != self._prev_algo: | |
c = messagebox.askyesno( | |
'Warning', | |
('Changing this will change all subsequent passwords. ' | |
'Are you sure you want to continue?') | |
) | |
if c is False: | |
self.algorithm_menu.set(self._prev_algo) | |
else: | |
self._prev_algo = algo | |
self.pw.set_algorithm(algo) | |
def make_password(self): | |
''' | |
spawns another thread which executes the password crunch in another | |
process. This is all done to avoid blocking tkinter. | |
''' | |
threading.Thread(target=self._make_password, daemon=True).start() | |
def _make_password(self): | |
''' | |
this function blocks | |
''' | |
# this is probably a bad way to do it, but whatever | |
# configure the pw object with out parameters: | |
d = [e.get() for e in self._entry_refs] | |
self.pw.data = d | |
# TODO: break out into multiprocessing in case of longer crunch times | |
p = ValuedThread(target=self.pw.make_password) | |
p.start() | |
p.join() | |
if p.error: | |
self.message_area.config(text=p.result, style='ER.TLabel') | |
self.password_area.config(text='') | |
self.password_area.show_text() # this should make it disappear | |
else: | |
self.message_area.config(text='') | |
self.password_area.config(text=p.result) | |
self.password_area.hide_text() | |
class App(Frame): | |
''' | |
Main view that handles the screen switching through callbacks | |
in daughter views. This view has no widgets of its own and | |
only acts as a container for the application views. | |
''' | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
# define views | |
self.about = About(self, callback=self.return_from_about) | |
self.info = InputInfo(self, about=self.show_about) | |
# this makes the grid fill the available space, like pack | |
self.grid_rowconfigure(0, weight=1) | |
self.grid_columnconfigure(0, weight=1) | |
self.about.grid_rowconfigure(0, weight=1) | |
self.about.grid_columnconfigure(0, weight=1) | |
self.info.grid_rowconfigure(0, weight=1) | |
self.info.grid_columnconfigure(0, weight=1) | |
# gridding them in the same cell allows tkraise to switch them | |
self.about.grid(column=0, row=0, sticky="nesw") | |
self.info.grid(column=0, row=0, sticky="nesw") | |
try: | |
open('confirmed').close() | |
self.info.tkraise() | |
except FileNotFoundError: | |
self.about.tkraise() | |
def return_from_about(self): | |
self.info.tkraise() | |
def show_about(self): | |
self.about.tkraise() | |
class Root(Tk): | |
''' | |
This is the root window that all the other widgets live inside of. | |
It handles the widgets' style and creates the main screen. | |
''' | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.geometry('400x320') | |
self.title('PW Anywhere v' + __version__) | |
self.style = Style(self) | |
try: | |
self.style.theme_use('vista') | |
except: | |
self.style.theme_use('clam') | |
self.style.configure("TEntry", padding=4) | |
self.style.configure("TLabel", padding=3) | |
self.style.configure("GF.TLabel", foreground="grey", font='helvitica 9') | |
self.style.configure("BB.TLabel", background="black", font='helvitica 15') | |
self.style.configure("WB.TLabel", background="white", font='helvitica 15') | |
self.style.configure("ER.TLabel", foreground="red", font='courier 10') | |
self.style.configure("M.TLabel", font='courier 10') | |
App(self).pack(expand=True, fill=BOTH) | |
if __name__ == '__main__': | |
Root().mainloop() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment