Skip to content

Instantly share code, notes, and snippets.

@0xpizza
Last active August 5, 2019 23:04
Show Gist options
  • Save 0xpizza/6fbba5faf1c8883394b10caf970941ef to your computer and use it in GitHub Desktop.
Save 0xpizza/6fbba5faf1c8883394b10caf970941ef to your computer and use it in GitHub Desktop.
#!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