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.
460 lines
19 KiB
460 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)
|
|
|
|
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()
|