You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

461 lines
19 KiB

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