root/software/pwmgr/pwmgr.py @ 849d4069
| 5a945a4a | dsorber | #!/usr/bin/python3
|
||
| cf157731 | dsorber | import glob
|
||
| b8436fc8 | dsorber | import hashlib
|
||
| 5a945a4a | dsorber | import os
|
||
import shutil
|
||||
import subprocess
|
||||
| feddc602 | dsorber | import sys
|
||
| 0b45b84e | dsorber | import threading
|
||
| feddc602 | dsorber | |||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
| 0b45b84e | dsorber | from gi.repository import Gtk, Pango, Gdk, GLib
|
||
| feddc602 | dsorber | |||
| 5a945a4a | dsorber | MOUNT_PATH = '/mnt/pw_ramdisk'
|
||
PWD_FILE_PATH = '/home/dsorber/Desktop/network'
|
||||
ENC_FILE_PATH = '/home/dsorber/Documents/chicken_soup.enc'
|
||||
SCRATCH_FILE_PATH = '/mnt/pw_ramdisk/chicken_soup.txt'
|
||||
ENC_TMP_FILE_PATH = '/home/dsorber/Documents/chicken_soup.enc.tmp'
|
||||
| cf157731 | dsorber | KEY_FILE_PATH = '/mnt/pw_ramdisk/.f05d11ef-000c-45c2-ae14-56b56c23e1c5'
|
||
| 5a945a4a | dsorber | |||
| dde05520 | dsorber | VERSION = '0.0.6'
|
||
| 5a945a4a | dsorber | |||
| feddc602 | dsorber | class SearchDialog(Gtk.Dialog):
|
||
def __init__(self, parent):
|
||||
| 5a945a4a | dsorber | Gtk.Dialog.__init__(self, 'Search', parent,
|
||
| feddc602 | dsorber | Gtk.DialogFlags.MODAL, buttons=(
|
||
Gtk.STOCK_FIND, Gtk.ResponseType.OK,
|
||||
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))
|
||||
box = self.get_content_area()
|
||||
| 5a945a4a | dsorber | label = Gtk.Label('Insert text you want to search for:')
|
||
| feddc602 | dsorber | box.add(label)
|
||
self.entry = Gtk.Entry()
|
||||
| cf157731 | dsorber | self.entry.set_activates_default(True)
|
||
| feddc602 | dsorber | box.add(self.entry)
|
||
| cf157731 | dsorber | # Make hitting ENTER trigger the OK button
|
||
ok_button = self.get_widget_for_response(response_id=Gtk.ResponseType.OK)
|
||||
ok_button.set_can_default(True)
|
||||
ok_button.grab_default()
|
||||
| feddc602 | dsorber | self.show_all()
|
||
| 5a945a4a | dsorber | |||
| 64a1e5a9 | dsorber | |||
| 0b45b84e | dsorber | class WarningDialog(Gtk.MessageDialog):
|
||
| 64a1e5a9 | dsorber | |||
def __init__(self, parent, title, message):
|
||||
Gtk.MessageDialog.__init__(self, parent,
|
||||
Gtk.DialogFlags.MODAL,
|
||||
Gtk.MessageType.WARNING,
|
||||
Gtk.ButtonsType.OK_CANCEL,
|
||||
message)
|
||||
self.set_title(title)
|
||||
| 0b45b84e | dsorber | |||
# Make hitting ENTER trigger the OK button
|
||||
ok_button = self.get_widget_for_response(response_id=Gtk.ResponseType.OK)
|
||||
ok_button.set_can_default(True)
|
||||
ok_button.grab_default()
|
||||
| 64a1e5a9 | dsorber | self.show_all()
|
||
| 5a945a4a | dsorber | def get_user_pw(parent, message, title=''):
|
||
"""
|
||||
Returns user input as a string or None
|
||||
If user does not input text it returns None, NOT AN EMPTY STRING.
|
||||
https://stackoverflow.com/questions/13970445/python-gtk3-how-to-add-a-gtk-entry-to-a-gtk-messagedialog
|
||||
"""
|
||||
| 97b0fc46 | dsorber | dialog = Gtk.MessageDialog(parent,
|
||
| 64a1e5a9 | dsorber | Gtk.DialogFlags.MODAL |
|
||
Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.QUESTION,
|
||||
Gtk.ButtonsType.OK_CANCEL,
|
||||
message)
|
||||
| cf157731 | dsorber | |||
| 97b0fc46 | dsorber | dialog.set_title(title)
|
||
| 5a945a4a | dsorber | |||
| 97b0fc46 | dsorber | box = dialog.get_content_area()
|
||
| 0b45b84e | dsorber | box.set_border_width(10)
|
||
| 97b0fc46 | dsorber | pw_entry = Gtk.Entry()
|
||
pw_entry.set_visibility(False)
|
||||
pw_entry.set_invisible_char('*')
|
||||
pw_entry.set_size_request(200, 0)
|
||||
| cf157731 | dsorber | pw_entry.set_activates_default(True)
|
||
| 97b0fc46 | dsorber | |||
box.add(pw_entry)
|
||||
#~ box.pack_end(pw_entry, False, False, 0)
|
||||
| cf157731 | dsorber | |||
# Make hitting ENTER trigger the OK button
|
||||
ok_button = dialog.get_widget_for_response(response_id=Gtk.ResponseType.OK)
|
||||
ok_button.set_can_default(True)
|
||||
ok_button.grab_default()
|
||||
| 5a945a4a | dsorber | |||
| 97b0fc46 | dsorber | dialog.show_all()
|
||
response = dialog.run()
|
||||
text = pw_entry.get_text()
|
||||
dialog.destroy()
|
||||
| 5a945a4a | dsorber | if (response == Gtk.ResponseType.OK) and (text != ''):
|
||
return text
|
||||
else:
|
||||
| 0b45b84e | dsorber | return None
|
||
class ProgressBarWindow(Gtk.Window):
|
||||
def __init__(self, parent, title):
|
||||
Gtk.Window.__init__(self, title=title)
|
||||
self.set_border_width(10)
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
|
||||
self.add(vbox)
|
||||
self.progressbar = Gtk.ProgressBar()
|
||||
vbox.pack_start(self.progressbar, True, True, 0)
|
||||
self.progressbar.set_text('Zee text')
|
||||
self.progressbar.set_show_text(True)
|
||||
self.set_modal(True)
|
||||
#~ self.set_decorated(False)
|
||||
self.set_deletable(False)
|
||||
self.set_transient_for(parent)
|
||||
def update_text(self, text):
|
||||
self.progressbar.set_text(text)
|
||||
def update_complete(self, fraction):
|
||||
self.progressbar.set_fraction(fraction)
|
||||
| feddc602 | dsorber | |||
class TextViewWindow(Gtk.Window):
|
||||
def __init__(self):
|
||||
| 5a945a4a | dsorber | Gtk.Window.__init__(self, title='pwmgr')
|
||
| feddc602 | dsorber | |||
| 5a945a4a | dsorber | self.set_default_size(800, 600)
|
||
| feddc602 | dsorber | |||
self.grid = Gtk.Grid()
|
||||
self.add(self.grid)
|
||||
self.create_textview()
|
||||
self.create_toolbar()
|
||||
| 5a945a4a | dsorber | self.create_bottom()
|
||
| dde05520 | dsorber | self.connect("key-press-event", self._key_press_event)
|
||
| 5a945a4a | dsorber | |||
self.mounted = False
|
||||
| 0b45b84e | dsorber | self.pass_data_loaded = False
|
||
| 5a945a4a | dsorber | self.pwd_file_fd = None
|
||
| b8436fc8 | dsorber | self.original_hash = None
|
||
| 0b45b84e | dsorber | self.pbar_win = None
|
||
self.should_quit = False
|
||||
| dde05520 | dsorber | |||
# Open password prompt window at start
|
||||
GLib.idle_add(self.open_pwd_file, self.grid)
|
||||
| 0b45b84e | dsorber | |||
def async_quit(self):
|
||||
if self.should_quit:
|
||||
if self.pbar_win is not None:
|
||||
self.pbar_win.destroy()
|
||||
Gtk.main_quit()
|
||||
print('REAL QUIT')
|
||||
return True
|
||||
| cf157731 | dsorber | |||
def _key_press_event(self, widget, event):
|
||||
keyval = event.keyval
|
||||
keyval_name = Gdk.keyval_name(keyval)
|
||||
state = event.state
|
||||
ctrl = (state & Gdk.ModifierType.CONTROL_MASK)
|
||||
# Bind existing handlers to keys
|
||||
if ctrl and keyval_name == 'f':
|
||||
self.on_search_clicked(widget)
|
||||
elif ctrl and keyval_name == 'o':
|
||||
self.open_pwd_file(widget)
|
||||
elif ctrl and keyval_name == 's':
|
||||
self.save_pwd_file(widget)
|
||||
elif ctrl and keyval_name == 'q':
|
||||
self.close_pwd_file(widget, None)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
| feddc602 | dsorber | |||
def create_toolbar(self):
|
||||
toolbar = Gtk.Toolbar()
|
||||
self.grid.attach(toolbar, 0, 0, 3, 1)
|
||||
| 5a945a4a | dsorber | button_open = Gtk.ToolButton()
|
||
button_open.set_icon_name('document-open')
|
||||
toolbar.insert(button_open, 0)
|
||||
button_open.connect('clicked', self.open_pwd_file)
|
||||
button_save = Gtk.ToolButton()
|
||||
button_save.set_icon_name('document-save')
|
||||
toolbar.insert(button_save, 1)
|
||||
button_save.connect('clicked', self.save_pwd_file)
|
||||
button_close = Gtk.ToolButton()
|
||||
button_close.set_icon_name('window-close')
|
||||
toolbar.insert(button_close, 2)
|
||||
button_close.connect('clicked', self.close_pwd_file, None)
|
||||
| feddc602 | dsorber | |||
toolbar.insert(Gtk.SeparatorToolItem(), 3)
|
||||
button_clear = Gtk.ToolButton()
|
||||
| 5a945a4a | dsorber | button_clear.set_icon_name('edit-clear-symbolic')
|
||
button_clear.connect('clicked', self.on_clear_clicked)
|
||||
toolbar.insert(button_clear, 4)
|
||||
| feddc602 | dsorber | |||
| 5a945a4a | dsorber | toolbar.insert(Gtk.SeparatorToolItem(), 5)
|
||
| feddc602 | dsorber | |||
button_search = Gtk.ToolButton()
|
||||
| 5a945a4a | dsorber | button_search.set_icon_name('system-search-symbolic')
|
||
button_search.connect('clicked', self.on_search_clicked)
|
||||
toolbar.insert(button_search, 6)
|
||||
| feddc602 | dsorber | |||
def create_textview(self):
|
||||
scrolledwindow = Gtk.ScrolledWindow()
|
||||
scrolledwindow.set_hexpand(True)
|
||||
scrolledwindow.set_vexpand(True)
|
||||
self.grid.attach(scrolledwindow, 0, 1, 3, 1)
|
||||
self.textview = Gtk.TextView()
|
||||
| 5a945a4a | dsorber | self.textview.set_editable(False)
|
||
self.textview.set_cursor_visible(False)
|
||||
| feddc602 | dsorber | self.textbuffer = self.textview.get_buffer()
|
||
scrolledwindow.add(self.textview)
|
||||
self.tag_bold = self.textbuffer.create_tag("bold",
|
||||
weight=Pango.Weight.BOLD)
|
||||
self.tag_italic = self.textbuffer.create_tag("italic",
|
||||
style=Pango.Style.ITALIC)
|
||||
self.tag_underline = self.textbuffer.create_tag("underline",
|
||||
underline=Pango.Underline.SINGLE)
|
||||
| 5a945a4a | dsorber | self.tag_found = self.textbuffer.create_tag('found',
|
||
| 64a1e5a9 | dsorber | background='orange',
|
||
weight=Pango.Weight.BOLD)
|
||||
| 5a945a4a | dsorber | |||
def create_bottom(self):
|
||||
label = Gtk.Label()
|
||||
label.set_text('version: {:s}'.format(VERSION))
|
||||
label.set_justify(Gtk.Justification.RIGHT)
|
||||
self.grid.attach(label, 0, 2, 3, 1)
|
||||
def open_pwd_file(self, widget):
|
||||
"""
|
||||
Open handler
|
||||
"""
|
||||
| cf157731 | dsorber | |||
| 5a945a4a | dsorber | # Get password
|
||
| 0b45b84e | dsorber | if not self.pass_data_loaded:
|
||
passwd = get_user_pw(self, 'Please enter your password', 'Password')
|
||||
if passwd is None:
|
||||
return
|
||||
| 97b0fc46 | dsorber | |||
| 5a945a4a | dsorber | if not self.mounted:
|
||
os.makedirs(MOUNT_PATH, 0o600, exist_ok=True)
|
||||
# Mount the tmpfs
|
||||
mnt_cmd = 'mount -t tmpfs -o size=16m tmpfs {:s}'
|
||||
mnt_cmd = mnt_cmd.format(MOUNT_PATH)
|
||||
subprocess.call(mnt_cmd, shell=True, timeout=5)
|
||||
self.mounted = True
|
||||
| cf157731 | dsorber | # Store password
|
||
with open(KEY_FILE_PATH, 'w') as pass_wd_file:
|
||||
pass_wd_file.write(passwd)
|
||||
os.chmod(KEY_FILE_PATH, 0o600)
|
||||
# Decrypt password store
|
||||
cmd = 'openssl enc -aes-256-cbc -d -in {:s} -out {:s} -pass file:{:s}'
|
||||
cmd = cmd.format(ENC_FILE_PATH, SCRATCH_FILE_PATH, KEY_FILE_PATH)
|
||||
| 5a945a4a | dsorber | subprocess.call(cmd, shell=True, timeout=5)
|
||
# Open and load file; make textview editable
|
||||
with open(SCRATCH_FILE_PATH, 'r') as pwd_file_fd:
|
||||
self.textbuffer.set_text(pwd_file_fd.read())
|
||||
self.textview.set_editable(True)
|
||||
self.textview.set_cursor_visible(True)
|
||||
| 0b45b84e | dsorber | |||
| b8436fc8 | dsorber | # Store hash of the original content which is used to detect changes
|
||
self.original_hash = self._hash_textview()
|
||||
| 0b45b84e | dsorber | # Mark state that pasword data is loaded
|
||
self.pass_data_loaded = True
|
||||
| 5a945a4a | dsorber | |||
def save_pwd_file(self, widget):
|
||||
"""
|
||||
Save handler
|
||||
"""
|
||||
| 0b45b84e | dsorber | if self.mounted and self.pass_data_loaded:
|
||
| b8436fc8 | dsorber | # Check if changes have been made
|
||
current_hash = self._hash_textview()
|
||||
if self.original_hash.hexdigest() != current_hash.hexdigest():
|
||||
# Changes were made, must save data
|
||||
# Write the password store data to the scratch file
|
||||
with open(SCRATCH_FILE_PATH, 'w') as pwd_file_fd:
|
||||
pwd_file_fd.write(
|
||||
self.textbuffer.get_text(self.textbuffer.get_start_iter(),
|
||||
self.textbuffer.get_end_iter(),
|
||||
True))
|
||||
print('Write complete')
|
||||
# Encrypt the password store
|
||||
cmd = 'openssl enc -aes-256-cbc -salt -in {:s} -out {:s} -pass'\
|
||||
' file:{:s}'
|
||||
cmd = cmd.format(SCRATCH_FILE_PATH, ENC_TMP_FILE_PATH,
|
||||
KEY_FILE_PATH)
|
||||
subprocess.call(cmd, shell=True, timeout=5)
|
||||
print('Enc complete')
|
||||
# Remove the key file, then copy over the encrypted file with
|
||||
# the just created temp one
|
||||
os.remove(KEY_FILE_PATH)
|
||||
os.remove(ENC_FILE_PATH)
|
||||
os.rename(ENC_TMP_FILE_PATH, ENC_FILE_PATH)
|
||||
| 5a945a4a | dsorber | |||
# Clear the buffer and disable the textview
|
||||
self.textbuffer.set_text('')
|
||||
self.textview.set_editable(False)
|
||||
self.textview.set_cursor_visible(False)
|
||||
| 0b45b84e | dsorber | |||
# Mark password data as not loaded
|
||||
self.pass_data_loaded = False
|
||||
| cf157731 | dsorber | |||
def _remove_mount_files(self):
|
||||
# Remove files in the mount path
|
||||
| dde05520 | dsorber | for file_object in os.listdir(MOUNT_PATH):
|
||
file_object_path = os.path.join(MOUNT_PATH, file_object)
|
||||
if os.path.isfile(file_object_path):
|
||||
os.unlink(file_object_path)
|
||||
else:
|
||||
shutil.rmtree(file_object_path)
|
||||
| cf157731 | dsorber | |||
| b8436fc8 | dsorber | def _hash_textview(self):
|
||
passwd_text = self.textbuffer.get_text(self.textbuffer.get_start_iter(),
|
||||
self.textbuffer.get_end_iter(),
|
||||
True)
|
||||
return hashlib.sha256(bytearray(passwd_text, 'utf-8'))
|
||||
| 0b45b84e | dsorber | def do_mount_cleanup(self):
|
||
| 5a945a4a | dsorber | """
|
||
| 0b45b84e | dsorber | Clean up ramdisk by overwriting, then unmount
|
||
| 5a945a4a | dsorber | """
|
||
| 0b45b84e | dsorber | GLib.idle_add(self.pbar_win.update_text, 'Starting ramdisk cleanup')
|
||
| 64a1e5a9 | dsorber | |||
| 5a945a4a | dsorber | rand_write_cmd = 'dd if=/dev/urandom of={:s} bs=16M count=1 status=none'
|
||
rand_write_cmd = rand_write_cmd.format(os.path.join(MOUNT_PATH, 'data'))
|
||||
zero_write_cmd = 'dd if=/dev/zero of={:s} bs=16M count=1 status=none'
|
||||
zero_write_cmd = zero_write_cmd.format(os.path.join(MOUNT_PATH, 'data'))
|
||||
| 0b45b84e | dsorber | self._remove_mount_files()
|
||
# Overwrite the ramdisk a few times
|
||||
for ctr in range(10):
|
||||
print(ctr)
|
||||
message = 'Overwrite {:d} of 10'.format(ctr + 1)
|
||||
GLib.idle_add(self.pbar_win.update_text, message)
|
||||
subprocess.call(rand_write_cmd, shell=True, timeout=5)
|
||||
self._remove_mount_files()
|
||||
subprocess.call(zero_write_cmd, shell=True, timeout=5)
|
||||
| cf157731 | dsorber | self._remove_mount_files()
|
||
| 5a945a4a | dsorber | |||
| 0b45b84e | dsorber | GLib.idle_add(self.pbar_win.update_complete, (ctr + 1) / 10)
|
||
GLib.idle_add(self.pbar_win.update_text, 'Unmounting ramdisk')
|
||||
umount_cmd = 'umount {:s}'.format(MOUNT_PATH)
|
||||
subprocess.call(umount_cmd, shell=True, timeout=5)
|
||||
GLib.idle_add(self.pbar_win.update_text, 'Complete')
|
||||
self.should_quit = True
|
||||
print('SET should quit')
|
||||
def close_pwd_file(self, widget, other):
|
||||
"""
|
||||
Close handler
|
||||
"""
|
||||
| b8436fc8 | dsorber | |||
| 0b45b84e | dsorber | if self.pass_data_loaded:
|
||
| b8436fc8 | dsorber | # Hash the current text to see if any changes have been made
|
||
current_hash = self._hash_textview()
|
||||
if current_hash.hexdigest() != self.original_hash.hexdigest():
|
||||
# Changes have been made, ask the user to save
|
||||
dialog = WarningDialog(self, 'Save changes?',
|
||||
'Save changes to password data?')
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
| 0b45b84e | dsorber | |||
| b8436fc8 | dsorber | if response == Gtk.ResponseType.OK:
|
||
self.save_pwd_file(widget)
|
||||
| 0b45b84e | dsorber | |||
# Clear the buffer and disable the textview
|
||||
self.textbuffer.set_text('')
|
||||
self.textview.set_editable(False)
|
||||
self.textview.set_cursor_visible(False)
|
||||
if self.mounted:
|
||||
# Start async polling of the quit flag
|
||||
| dde05520 | dsorber | GLib.timeout_add(500, self.async_quit)
|
||
| 0b45b84e | dsorber | |||
# Create the progress window
|
||||
self.pbar_win = ProgressBarWindow(self, 'Closing...')
|
||||
self.pbar_win.show_all()
|
||||
| 5a945a4a | dsorber | |||
| 0b45b84e | dsorber | # Start the work thread that does the cleanup
|
||
thread = threading.Thread(target=self.do_mount_cleanup)
|
||||
thread.start()
|
||||
else:
|
||||
Gtk.main_quit()
|
||||
| 5a945a4a | dsorber | |||
print('CLOSE')
|
||||
| feddc602 | dsorber | |||
def on_clear_clicked(self, widget):
|
||||
start = self.textbuffer.get_start_iter()
|
||||
end = self.textbuffer.get_end_iter()
|
||||
self.textbuffer.remove_all_tags(start, end)
|
||||
def on_search_clicked(self, widget):
|
||||
dialog = SearchDialog(self)
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
cursor_mark = self.textbuffer.get_insert()
|
||||
start = self.textbuffer.get_iter_at_mark(cursor_mark)
|
||||
if start.get_offset() == self.textbuffer.get_char_count():
|
||||
start = self.textbuffer.get_start_iter()
|
||||
self.search_and_mark(dialog.entry.get_text(), start)
|
||||
dialog.destroy()
|
||||
def search_and_mark(self, text, start):
|
||||
end = self.textbuffer.get_end_iter()
|
||||
match = start.forward_search(text, 0, end)
|
||||
if match != None:
|
||||
| cf157731 | dsorber | match_start, match_end = match
|
||
| feddc602 | dsorber | self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
|
||
| 64a1e5a9 | dsorber | |||
# Scroll so the match appears in the middle of the screen
|
||||
self.textview.scroll_to_iter(match_start, 0.1, True, 0.0, 0.5)
|
||||
#~ self.search_and_mark(text, match_end)
|
||||
| feddc602 | dsorber | |||
def main():
|
||||
| 5a945a4a | dsorber | """
|
||
NOTE: this *must* be called using sudo or gksudo
|
||||
"""
|
||||
| feddc602 | dsorber | win = TextViewWindow()
|
||
| 5a945a4a | dsorber | win.connect('delete-event', win.close_pwd_file)
|
||
| feddc602 | dsorber | win.show_all()
|
||
Gtk.main()
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|