Project

General

Profile

Download (18.7 KB) Statistics
| Branch: | Tag: | Revision:
#!/usr/bin/python3
import glob
import hashlib
import os
import shutil
import subprocess
import sys
import threading

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango, Gdk, GLib

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'
KEY_FILE_PATH = '/mnt/pw_ramdisk/.f05d11ef-000c-45c2-ae14-56b56c23e1c5'

VERSION = '0.0.9-beta'

class BetterSearchWindow(Gtk.Window):

def __init__(self, parent):
self.parent = parent
Gtk.Window.__init__(self, title='Search')
self.set_default_size(400, 140)
self.set_border_width(10)

vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
self.add(vbox)
# ~ search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=1)
search_grid = Gtk.Grid()
search_grid.set_column_spacing(10)
search_label = Gtk.Label('Search for:')
search_grid.attach(search_label, 0, 0, 1, 1)
self.search_entry = Gtk.Entry()
self.search_entry.set_hexpand(True)
search_grid.attach(self.search_entry, 1, 0, 1, 1)
vbox.pack_start(search_grid, True, True, 4)
# Stats display
self.stats_label = Gtk.Label('Matches found: ')
vbox.pack_start(self.stats_label, True, True, 0)

# Buttons
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
self.close_button = Gtk.Button.new_with_label('Close')
button_box.pack_start(self.close_button, True, True, 0)
self.close_button.connect('clicked', self.on_close_clicked)
self.next_button = Gtk.Button.new_with_label('Next')
button_box.pack_start(self.next_button, True, True, 0)
self.next_button.connect('clicked', self.on_next_clicked)
vbox.pack_start(button_box, False, True, 0)

def on_close_clicked(self, widget):
self.destroy()

def on_next_clicked(self, widget):
search_term = self.search_entry.get_text().strip()
print('Search for: '.format(search_term))
# Get start iterator
cursor_mark = self.parent.textbuffer.get_insert()
start = self.parent.textbuffer.get_iter_at_mark(cursor_mark)
if start.get_offset() == self.parent.textbuffer.get_char_count():
start = self.parent.textbuffer.get_start_iter()

# ~ self.search_and_mark(dialog.entry.get_text(), start)

class SearchDialog(Gtk.Dialog):

def __init__(self, parent):
Gtk.Dialog.__init__(self, 'Search', parent,
Gtk.DialogFlags.MODAL, buttons=(
Gtk.STOCK_FIND, Gtk.ResponseType.OK,
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))

box = self.get_content_area()

label = Gtk.Label('Insert text you want to search for:')
box.add(label)

self.entry = Gtk.Entry()
self.entry.set_activates_default(True)
box.add(self.entry)

# 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()

self.show_all()
class WarningDialog(Gtk.MessageDialog):
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)
# 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()
self.show_all()
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
"""
dialog = Gtk.MessageDialog(parent=parent,
modal=True,
destroy_with_parent=True,
message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.OK_CANCEL,
text=message)
dialog.set_title(title)

box = dialog.get_content_area()
box.set_border_width(10)
pw_entry = Gtk.Entry()
pw_entry.set_visibility(False)
pw_entry.set_invisible_char('*')
pw_entry.set_size_request(200, 0)
pw_entry.set_activates_default(True)
box.add(pw_entry)
#~ box.pack_end(pw_entry, False, False, 0)
# 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()

dialog.show_all()
response = dialog.run()
text = pw_entry.get_text()
dialog.destroy()
if (response == Gtk.ResponseType.OK) and (text != ''):
return text
else:
return None
def yield_gtk():
while Gtk.events_pending():
Gtk.main_iteration()
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)


class TextViewWindow(Gtk.Window):

def __init__(self):
Gtk.Window.__init__(self, title='pwmgr')

self.set_default_size(800, 600)

self.grid = Gtk.Grid()
self.add(self.grid)

self.create_textview()
self.create_toolbar()
self.create_bottom()
self.connect("key-press-event", self._key_press_event)
self.mounted = False
self.pass_data_loaded = False
self.pwd_file_fd = None
self.original_hash = None
self.pbar_win = None

# Open password prompt window at start
self.open_pwd_file(self.grid)
yield_gtk()

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

def create_toolbar(self):
toolbar = Gtk.Toolbar()
self.grid.attach(toolbar, 0, 0, 3, 1)

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)

toolbar.insert(Gtk.SeparatorToolItem(), 3)

button_clear = Gtk.ToolButton()
button_clear.set_icon_name('edit-clear-symbolic')
button_clear.connect('clicked', self.on_clear_clicked)
toolbar.insert(button_clear, 4)

toolbar.insert(Gtk.SeparatorToolItem(), 5)

button_search = Gtk.ToolButton()
button_search.set_icon_name('system-search-symbolic')
button_search.connect('clicked', self.on_search_clicked)
toolbar.insert(button_search, 6)
button_new_search = Gtk.ToolButton()
button_new_search.set_icon_name('system-search-symbolic')
button_new_search.connect('clicked', self.on_new_search_clicked)
toolbar.insert(button_new_search, 7)

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()
self.textview.set_editable(False)
self.textview.set_cursor_visible(False)
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)
self.tag_found = self.textbuffer.create_tag('found',
background='orange',
weight=Pango.Weight.BOLD)

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
"""

# Get password
if not self.pass_data_loaded:
passwd = get_user_pw(self, 'Please enter your password', 'Password')
if passwd is None:
return
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
# 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)
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)
# Store hash of the original content which is used to detect changes
self.original_hash = self._hash_textview()
# Mark state that pasword data is loaded
self.pass_data_loaded = True
def save_pwd_file(self, widget):
"""
Save handler
"""
if self.mounted and self.pass_data_loaded:
# 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)
# Clear the buffer and disable the textview
self.textbuffer.set_text('')
self.textview.set_editable(False)
self.textview.set_cursor_visible(False)
# Mark password data as not loaded
self.pass_data_loaded = False
def _remove_mount_files(self):
""" Helper function to remove files in the mount path
"""
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)
def _hash_textview(self):
""" Helper function to hash the contents of the textview
"""
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'))
def do_mount_cleanup(self):
"""
Clean up ramdisk by overwriting, then unmount
"""
self.pbar_win.update_text('Starting ramdisk cleanup')
yield_gtk()
# Build shell commands to write random and zero data (respectively) to
# the file
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'))
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)
self.pbar_win.update_text(message)
yield_gtk()
# Do one pass, write random data then write zeros
subprocess.call(rand_write_cmd, shell=True, timeout=5)
self._remove_mount_files()
subprocess.call(zero_write_cmd, shell=True, timeout=5)
self._remove_mount_files()
# Update progress bar
self.pbar_win.update_complete((ctr + 1) / 10)
yield_gtk()
self.pbar_win.update_text('Unmounting ramdisk')
yield_gtk()
# Unmount the ramdisk
umount_cmd = 'umount {:s}'.format(MOUNT_PATH)
subprocess.call(umount_cmd, shell=True, timeout=5)

self.pbar_win.update_text('Complete')
yield_gtk()
def close_pwd_file(self, widget, other):
"""
Close handler
"""
if self.pass_data_loaded:
# 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()
if response == Gtk.ResponseType.OK:
self.save_pwd_file(widget)
# 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:
# Create the progress window
self.pbar_win = ProgressBarWindow(self, 'Closing...')
self.pbar_win.show_all()
yield_gtk()
self.do_mount_cleanup()
# Quit GTK app
Gtk.main_quit()
print('CLOSE')

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):
# Remove existing marks before starting new search
self.textbuffer.remove_all_tags(self.textbuffer.get_start_iter(),
self.textbuffer.get_end_iter())
end = self.textbuffer.get_end_iter()
match = start.forward_search(text, Gtk.TextSearchFlags.CASE_INSENSITIVE, end)

if match != None:
match_start, match_end = match
self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
# 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)
def on_new_search_clicked(self, widget):
print('New search!')
search_win = BetterSearchWindow(self)
search_win.show_all()

def main():
"""
NOTE: this *must* be called using sudo or gksudo
"""
win = TextViewWindow()
win.connect('delete-event', win.close_pwd_file)
win.show_all()
Gtk.main()
if __name__ == '__main__':
sys.exit(main())
    (1-1/1)