#!/usr/bin/python3 """ A mobile first interface for the standard unix password manager written in python """ import os import tempfile from typing import Union import wx import wx.lib.scrolledpanel as scrolled import wx.lib.dialogs as dialogs import gnupg from pass_handler import Pass, copy_to_clipboard, get_password_from_path, pass_pull, pass_push, run_command class PassUi(wx.Frame): """ The wx.Frame for passui """ def redraw(*args): """Decorator used for redrawing the widgets in the sizer Returns: function: The decorated function """ func = args[0] def wrapper(self, *wrapper_args): """The wrapper function for the decorator """ self.sizer.Clear(delete_windows=True) func(self, *wrapper_args) self.pnl.SetupScrolling() self.sizer.Layout() return wrapper def __init__(self, *args, **kw): """__init__. :param args: :param kw: """ super().__init__(*args, **kw) self.file_str: str = str() self.pass_handler: Pass = Pass() self.gpg_handler: gnupg.GPG = gnupg.GPG() self.gpg_key: str = str() # create a panel in the frame self.pnl: wx.lib.scrolledpanel.ScrolledPanel = scrolled.ScrolledPanel(self, -1, style=wx.VSCROLL) self.pnl.SetupScrolling() # and create a sizer to manage the layout of child widgets self.sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) self.pnl.SetSizer(self.sizer) if self.pass_handler.is_init: self.add_buttons() else: self.add_init() @redraw def add_buttons(self): """add_buttons.""" self.add_tools() if self.pass_handler.cur_dir != self.pass_handler.top_dir: btn: wx.Button = self.make_back_button() self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event: self.path_button_clicked(), btn) index: int = 0 for c_path in self.pass_handler.cur_paths: if c_path != self.pass_handler.cur_dir: label: str = '๐Ÿ—€ ' + os.path.basename(os.path.normpath(c_path)) btn: wx.Button = wx.Button(self.pnl, label=label) self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, path=c_path: self.path_button_clicked( path), btn) index = index + 1 index = 0 for password in self.pass_handler.cur_passwords: label: str = '๐Ÿ— ' + os.path.splitext( os.path.basename(os.path.normpath(password)))[0] btn: wx.Button = wx.Button(self.pnl, label=label) self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, m_index=index: self.password_button_clicked( m_index), btn) index = index + 1 @redraw def add_init(self): """add_init""" select_label: str = "Select GPG Key" if self.gpg_key: label: wx.StaticText = wx.StaticText(self.pnl, label="Selected GPG key:") self.sizer.Add(label, 0, wx.EXPAND) # pylint: disable=no-member choice: wx.StaticText = wx.StaticText(self.pnl, label=self.gpg_key) self.sizer.Add(choice, 0, wx.EXPAND) # pylint: disable=no-member select_label: str = "Select New GPG Key" gpg_btn: wx.Button = wx.Button(self.pnl, label=select_label) self.sizer.Add(gpg_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event: self.gpg_button_clicked(), gpg_btn) init_btn: wx.Button = wx.Button(self.pnl, label="Init Local Password Store") git_btn: wx.Button = wx.Button(self.pnl, label="Clone Remote Password Store From Git") self.sizer.Add(init_btn, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(git_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event: self.git_button_clicked(), git_btn) if self.gpg_key: self.Bind(wx.EVT_BUTTON, lambda event: self.init_button_clicked(), init_btn) else: self.Bind(wx.EVT_BUTTON, lambda event: dialogs.alertDialog(message='You must select a GPG key'), init_btn) def add_push_pull(self): """add_push_pull.""" push_btn: wx.Button = wx.Button(self.pnl, label="Push to remote") self.sizer.Add(push_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event: pass_push(), push_btn) pull_btn: wx.Button = wx.Button(self.pnl, label="Pull from remote") self.sizer.Add(pull_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event: pass_pull(), pull_btn) def add_tools(self, index: Union[None, int] = None): """add_tools. :param index: Union[None.int] """ btn: wx.Button = wx.Button(self.pnl, label="Show tools") font: wx.Font = btn.GetFont().MakeBold() btn.SetFont(font) self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, m_index=index: self.show_tools(m_index), btn) def back_button_clicked(self, index: Union[None, int] = None): """back_button_clicked. :param index: Union[None.int] """ if index: self.show_password_dialog(index) else: self.add_buttons() def choice_button_clicked(self, event): """choice_button_clicked :param event: wx.Event """ choice = event.GetEventObject() self.gpg_key = choice.GetString(choice.GetSelection()) print(self.gpg_key) self.add_init() def delete_password(self, index: int): """delete_password. :param index: int """ path: str = self.pass_handler.get_pass_path_from_index(index, "password") dlg: wx.MessageDialog = wx.MessageDialog(self.pnl, "Delete " + path + "?", "Are you sure?", style=wx.CANCEL | wx.CANCEL_DEFAULT | wx.OK) dlg.SetOKCancelLabels("&Yes", "&Don't delete") reply: int = dlg.ShowModal() if reply == wx.ID_CANCEL: return self.pass_handler.delete_password(path) self.back_button_clicked() def git_button_clicked(self): """git_button_clicked. """ git_repo: str = self.show_git_picker() # self.pass_handler.pass_init(self.gpg_key, git_repo) # self.add_buttons() def git_submit_btn_clicked(self, widget_list): default_ports = {'git': 9418, 'git+ssh': 22, 'https': 443} user: str = widget_list[1].GetLineText(0) password: str = widget_list[3].GetLineText(0) protocol: str = widget_list[5].GetString(widget_list[5].GetSelection()) host: str = widget_list[7].GetLineText(0) port: str = widget_list[9].GetLineText(0) path: str = widget_list[11].GetLineText(0) if not port: port = str(default_ports[protocol]) self.file_str = 'protocol={}\nhost={}:{}{}\nusername=\npassword={}\n'.format(protocol, host, port, path, user, password) timeout_in_sec = 24 * 60 * 60 file_descriptor, file_name = tempfile.mkstemp(text=True) with open(file_name, 'w') as file: file.write(self.file_str) os.close(file_descriptor) run_command(['/usr/bin/git', 'credential-store', '--file', file_name]) run_command( ['/usr/bin/git', 'config', 'credential.helper', 'cache --timeout={}'.format(timeout_in_sec)]) os.unlink(file_name) self.init_button_clicked('{}://{}:{}{}'.format(protocol, host, port, path)) def gpg_button_clicked(self): """gpg_button_clicked. """ private_keys: dict = self.gpg_handler.list_keys(True) uid_list: list[str] = list() for key in private_keys: for uid in key['uids']: uid_list.append(uid) self.show_choice(uid_list, "Select GNUPG Key") def init_button_clicked(self, repo: Union[str, None] = None): """init_button_clicked. """ self.pass_handler.pass_init(self.gpg_key, repo) self.add_buttons() def make_back_button(self, index: Union[None, int] = None) -> wx.Button: """make_back_button. :param index: """ if index is not None: label: str = self.pass_handler.get_pass_path_from_index(index, "password") else: label: str = self.pass_handler.cur_dir.replace(self.pass_handler.top_dir, '') btn: wx.Button = wx.Button(self.pnl, label=label + 'โคด') font: wx.Font = btn.GetFont().MakeItalic().MakeBold() btn.SetFont(font) return btn def password_button_clicked(self, index: int): """password_button_clicked. :param index: """ self.show_password_dialog(index) def path_button_clicked(self, path: Union[None, str] = None): """path_button_clicked. :param path: """ if path is None: path = os.path.abspath(os.path.join(self.pass_handler.cur_dir, os.pardir)) self.pass_handler.cur_dir = path self.pass_handler.cur_paths = self.pass_handler.get_pass_paths() self.pass_handler.cur_passwords = self.pass_handler.get_pass_passwords() self.add_buttons() def save_to_pass(self, path: str, text: wx.TextCtrl, name: Union[None, wx.TextCtrl] = None): """save_to_pass. :param path: :param text: :param name: """ full_path: str = os.path.dirname(self.pass_handler.top_dir + '/' + path.lstrip('/')) if name is not None: path: str = name.GetLineText(0) full_path: str = os.path.dirname(self.pass_handler.top_dir + '/' + path.lstrip('/')) filename: str = full_path + '.gpg' if os.path.exists(full_path) or os.path.exists(filename): dlg: wx.MessageDialog = wx.MessageDialog( self.pnl, "Path: " + path + " already exists! Please update password path", "Error", style=wx.OK) dlg.ShowModal() reply: int = wx.ID_CANCEL else: dlg: wx.MessageDialog = wx.MessageDialog(self.pnl, "Save to " + path + "?", "Are you sure?", style=wx.CANCEL | wx.CANCEL_DEFAULT | wx.OK) dlg.SetOKCancelLabels("&Yes", "&Don't save") reply: int = dlg.ShowModal() if reply == wx.ID_CANCEL: return password: str = str() for line_no in range(text.GetNumberOfLines()): password += text.GetLineText(line_no) password += '\n' # FIXME: Is this right? Maybe we break stuff with the trailing newline? self.pass_handler.save_to_pass(password, path, full_path) self.back_button_clicked() @redraw def show_choice(self, choices: list[str], name: str): choice: wx.Choice = wx.Choice(self.pnl, choices=choices, name=name) self.sizer.Add(choice, 0, wx.EXPAND) self.Bind(wx.EVT_CHOICE, lambda event: self.choice_button_clicked(event), choice) @redraw def show_new_dialog(self): """show_new_dialog. """ pass_path: str = self.pass_handler.cur_dir.replace(self.pass_handler.top_dir, '') btn: wx.Button = self.make_back_button() self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, path=self.pass_handler.cur_dir: self.path_button_clicked( path), btn) name_sizer: wx.BoxSizer = wx.BoxSizer(orient=wx.VERTICAL) # pylint: disable=no-member name_sizer.Add(wx.StaticText(self.pnl, -1, "Password path:"), 0, wx.EXPAND) # pylint: disable=no-member pw_sizer: wx.BoxSizer = wx.BoxSizer(orient=wx.VERTICAL) # pylint: disable=no-member pw_sizer.Add(wx.StaticText(self.pnl, -1, "Password:"), 0, wx.EXPAND) # pylint: disable=no-member name: wx.TextCtrl = wx.TextCtrl(self.pnl, value=pass_path, style=wx.TE_DONTWRAP) text: wx.TextCtrl = wx.TextCtrl(self.pnl, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) s_btn: wx.Button = wx.Button(self.pnl, label="Save password") name_sizer.Add(name, 0, wx.EXPAND) # pylint: disable=no-member pw_sizer.Add(text, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(name_sizer, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(pw_sizer, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(s_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, path=pass_path, m_text=text, m_name=name: self. save_to_pass(path, m_text, m_name), s_btn) name.SetFocus() @redraw def show_password_dialog(self, index: int): """show_password_dialog. :param index: """ self.add_tools(index) pass_path: str = self.pass_handler.get_pass_path_from_index(index, "password") c_path: str = self.pass_handler.top_dir + os.path.dirname(pass_path) password: str = get_password_from_path(pass_path) btn: wx.Button = self.make_back_button(index) self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind( wx.EVT_BUTTON, lambda event, path=c_path: self.path_button_clicked(path), btn) s_btn: wx.Button = wx.Button(self.pnl, label="Show/edit password") c_btn: wx.Button = wx.Button(self.pnl, label="Copy password") d_btn: wx.Button = wx.Button(self.pnl, label="Delete password") self.sizer.Add(s_btn, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(c_btn, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(d_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, text=password: copy_to_clipboard(text), c_btn) self.Bind( wx.EVT_BUTTON, lambda event, m_index=index: self.show_password(m_index), s_btn) self.Bind( wx.EVT_BUTTON, lambda event, m_index=index: self.delete_password(m_index), d_btn) @redraw def show_password(self, index: int): """show_password. :param index: """ self.add_tools(index) pass_path: str = self.pass_handler.get_pass_path_from_index(index, "password") c_path: str = self.pass_handler.top_dir + os.path.dirname(pass_path) password: str = get_password_from_path(pass_path) btn: wx.Button = self.make_back_button(index) self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind( wx.EVT_BUTTON, lambda event, path=c_path: self.path_button_clicked(path), btn) text: wx.TextCtrl = wx.TextCtrl(self.pnl, value=password, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) c_btn: wx.Button = wx.Button(self.pnl, label="Copy password") s_btn: wx.Button = wx.Button(self.pnl, label="Save password") self.sizer.Add(text, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(c_btn, 0, wx.EXPAND) # pylint: disable=no-member self.sizer.Add(s_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, m_text=password: copy_to_clipboard(m_text), c_btn) self.Bind(wx.EVT_BUTTON, lambda event, path=pass_path, m_text=text: self.save_to_pass( path, m_text), s_btn) @redraw def show_tools(self, index: Union[None, int] = None): """show_tools. :param index: """ btn: wx.Button = wx.Button(self.pnl, label="Go back") font: wx.Font = btn.GetFont().MakeBold() btn.SetFont(font) self.sizer.Add(btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind( wx.EVT_BUTTON, lambda event, m_index=index: self.back_button_clicked(m_index), btn) n_btn: wx.Button = wx.Button(self.pnl, label="Add new password") font: wx.Font = n_btn.GetFont().MakeBold() n_btn.SetFont(font) self.sizer.Add(n_btn, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event: self.show_new_dialog(), n_btn) self.add_push_pull() @redraw def show_git_picker(self): widget_list: list[Union[wx.StaticText, wx.TextCtrl, wx.Button]] = list() widget_list.append(wx.StaticText(self.pnl, label='Username:')) widget_list.append(wx.TextCtrl(self.pnl)) widget_list.append(wx.StaticText(self.pnl, label='Password:')) widget_list.append(wx.TextCtrl(self.pnl, style=wx.TE_PASSWORD)) widget_list.append(wx.StaticText(self.pnl, label='Protocol:')) widget_list.append(wx.Choice(self.pnl, choices=['https', 'git', 'git+ssh'])) widget_list[5].SetSelection(0) widget_list.append(wx.StaticText(self.pnl, label='Hostname:')) widget_list.append(wx.TextCtrl(self.pnl, value='example.org')) widget_list.append(wx.StaticText(self.pnl, label='Port:')) widget_list.append(wx.TextCtrl(self.pnl, value='443')) widget_list.append(wx.StaticText(self.pnl, label='Path to repo:')) widget_list.append(wx.TextCtrl(self.pnl, value='/')) widget_list.append(wx.Button(self.pnl, label="Submit")) for widget in widget_list: self.sizer.Add(widget, 0, wx.EXPAND) # pylint: disable=no-member self.Bind(wx.EVT_BUTTON, lambda event, m_widgets=widget_list: self.git_submit_btn_clicked(m_widgets), widget_list[12]) if __name__ == '__main__': # When this module is run (not imported) then create the app, the # frame, show it, and start the event loop. app: wx.App = wx.App() frm: PassUi = PassUi(None, title='PassUi') frm.Show() app.MainLoop()