diff --git a/.gitignore b/.gitignore index bab7d6d..add7776 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ __pycache__ .flatpak-builder flatpak-pip-generator repo -__debug* \ No newline at end of file +__debug* +.templates diff --git a/com.quexten.Goldwarden.yml b/com.quexten.Goldwarden.yml index ac801f1..b177f29 100644 --- a/com.quexten.Goldwarden.yml +++ b/com.quexten.Goldwarden.yml @@ -25,6 +25,13 @@ finish-args: # biometric / user password auth - --system-talk-name=org.freedesktop.PolicyKit1 modules: + - name: "blueprint-compiler" + buildsystem: meson + cleanup: ['*'] + sources: + - type: git + url: https://gitlab.gnome.org/jwestman/blueprint-compiler + tag: v0.12.0 - ./gui/python3-requirements.json - name: goldwarden-python-ui buildsystem: simple @@ -35,6 +42,7 @@ modules: - install -D ./gui/com.quexten.Goldwarden.desktop /app/share/applications/com.quexten.Goldwarden.desktop - install -D ./gui/goldwarden.svg /app/share/icons/hicolor/scalable/apps/com.quexten.Goldwarden.svg - install -Dm644 ./gui/com.quexten.Goldwarden.metainfo.xml -t /app/share/metainfo/ + - blueprint-compiler batch-compile /app/bin/src/gui/.templates/ /app/bin/src/gui/ /app/bin/src/gui/*.blp sources: - type: dir path: ./ diff --git a/gui/src/gui/browserbiometrics.blp b/gui/src/gui/browserbiometrics.blp new file mode 100644 index 0000000..af50892 --- /dev/null +++ b/gui/src/gui/browserbiometrics.blp @@ -0,0 +1,44 @@ +using Gtk 4.0; +using Adw 1; + +Adw.Window window { + default-width: 500; + default-height: 500; + + Adw.ToolbarView view { + content: Box{ + orientation: vertical; + ScrolledWindow { + vexpand: true; + hexpand: true; + child: Box content { + orientation: vertical; + Adw.PreferencesPage preferences_page { + title: "General"; + + Adw.PreferencesGroup register_browser_biometrics_group { + title: "Register Browser Biometrics"; + description: "Run the following command in your terminal to set up the browser biometrics integration"; + + Adw.ActionRow setup_command_row { + subtitle: "flatpak run --filesystem=home --command=goldwarden com.quexten.Goldwarden setup browserbiometrics"; + subtitle-selectable: true; + } + } + } + }; + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Goldwarden Browser Biometrics Setup'; + }; + + valign: start; + } + } +} \ No newline at end of file diff --git a/gui/src/gui/browserbiometrics.py b/gui/src/gui/browserbiometrics.py index 3199786..289f1b1 100644 --- a/gui/src/gui/browserbiometrics.py +++ b/gui/src/gui/browserbiometrics.py @@ -1,53 +1,32 @@ +#!/usr/bin/env python3 +import sys import gi + gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -import gc -import time -from gi.repository import Gtk, Adw, GLib, Notify, Gdk -from threading import Thread -import sys -import os -from . import components -class MyApp(Adw.Application): +from gi.repository import Gtk, Adw, GLib, Gdk, Gio +from ..services import goldwarden +from threading import Thread +from .template_loader import load_template +import subprocess +from . import components +import os + +class GoldwardenBrowserBiometricsSetupGuideApp(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) def on_activate(self, app): - self.pinentry_window = MainWindow(application=app) - self.pinentry_window.present() - self.app = app + self.load() + self.window.present() -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def load(self): + builder = load_template("browserbiometrics.ui") + self.window = builder.get_object("window") + self.window.set_application(self) - # vertical box - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.VERTICAL) - self.set_child(self.box) - - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.box.append(self.stack) - - self.preferences_page = Adw.PreferencesPage() - self.preferences_page.set_title("General") - self.stack.add_named(self.preferences_page, "preferences_page") - - self.register_browser_biometrics_group = Adw.PreferencesGroup() - self.register_browser_biometrics_group.set_title("Register Browser Biometrics") - self.register_browser_biometrics_group.set_description("Run the following command in your terminal to set up the browser biometrics integration") - self.preferences_page.add(self.register_browser_biometrics_group) - - self.setup_command_row = Adw.ActionRow() - self.setup_command_row.set_subtitle("flatpak run --filesystem=home --command=goldwarden com.quexten.Goldwarden setup browserbiometrics") - self.setup_command_row.set_subtitle_selectable(True) - self.register_browser_biometrics_group.add(self.setup_command_row) - - self.set_default_size(700, 400) - self.set_title("Goldwarden Browser Biometrics Setup") - -app = MyApp(application_id="com.quexten.Goldwarden.browserbiometrics") -app.run(sys.argv) \ No newline at end of file +if __name__ == "__main__": + app = GoldwardenBrowserBiometricsSetupGuideApp(application_id="com.quexten.Goldwarden.browserbiometrics") + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/login.blp b/gui/src/gui/login.blp new file mode 100644 index 0000000..0cc752e --- /dev/null +++ b/gui/src/gui/login.blp @@ -0,0 +1,69 @@ +using Gtk 4.0; +using Adw 1; + +Adw.Window window { + default-width: 400; + default-height: 550; + + Adw.ToolbarView view { + content: Box { + orientation: vertical; + spacing: 6; + + Adw.PreferencesPage preferences_page { + Adw.PreferencesGroup { + title: "Authentication"; + margin-start: 12; + margin-end: 12; + margin-bottom: 12; + Adw.EntryRow email_row { + title: "Email"; + text: ""; + } + Adw.EntryRow client_id_row { + title: "Client ID (optional)"; + text: ""; + } + Adw.EntryRow client_secret_row { + title: "Client Secret (optional)"; + text: ""; + } + } + Adw.PreferencesGroup { + title: "Server"; + margin-start: 12; + margin-end: 12; + margin-top: 12; + margin-bottom: 12; + Adw.EntryRow server_row { + title: "Server URL"; + text: "https://vault.bitwarden.com"; + } + } + Adw.PreferencesGroup { + margin-start: 12; + margin-end: 12; + margin-top: 12; + margin-bottom: 12; + Button login_button { + label: "Login"; + styles [ + "suggested-action" + ] + } + } + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Goldwarden Login'; + }; + + valign: start; + } + } +} diff --git a/gui/src/gui/login.py b/gui/src/gui/login.py new file mode 100644 index 0000000..a062b1a --- /dev/null +++ b/gui/src/gui/login.py @@ -0,0 +1,66 @@ +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +import gc +import time +from gi.repository import Gtk, Adw, GLib, Notify, Gdk +from threading import Thread +from .template_loader import load_template +import sys +import os +from ..services import goldwarden + +goldwarden.create_authenticated_connection(None) + +class GoldwardenLoginApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.connect('activate', self.on_activate) + + def on_activate(self, app): + self.load() + self.window.present() + + def load(self): + builder = load_template("login.ui") + self.window = builder.get_object("window") + self.window.set_application(self) + self.email_row = builder.get_object("email_row") + self.client_id_row = builder.get_object("client_id_row") + self.client_secret_row = builder.get_object("client_secret_row") + self.server_row = builder.get_object("server_row") + self.login_button = builder.get_object("login_button") + self.login_button.connect("clicked", lambda x: self.on_login()) + + evk = Gtk.EventControllerKey.new() + evk.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + evk.connect("key-pressed", self.key_press) + self.window.add_controller(evk) + + def key_press(self, event, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: + os._exit(0) + + if keyval == Gdk.KEY_Return and state & Gdk.ModifierType.CONTROL_MASK: + self.on_login() + return True + + def on_login(self): + email = self.email_row.get_text() + client_id = self.client_id_row.get_text() + client_secret = self.client_secret_row.get_text() + server = self.server_row.get_text() + goldwarden.set_url(server) + if client_id != "": + goldwarden.set_client_id(client_id) + if client_secret != "": + goldwarden.set_client_secret(client_secret) + goldwarden.login_with_password(email, "") + self.window.close() + +if __name__ == "__main__": + settings = Gtk.Settings.get_default() + settings.set_property("gtk-error-bell", False) + + app = GoldwardenLoginApp(application_id="com.quexten.Goldwarden.login") + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/main_view.blp b/gui/src/gui/main_view.blp new file mode 100644 index 0000000..e6ea21b --- /dev/null +++ b/gui/src/gui/main_view.blp @@ -0,0 +1,43 @@ +using Gtk 4.0; +using Adw 1; + +Adw.ToolbarView view { + content: Box{ + orientation: vertical; + Adw.Banner paused_banner { + title: 'Backups paused due to Power Saver Mode'; + revealed: false; + } + + ScrolledWindow { + vexpand: true; + hexpand: true; + margin-top: 20; + child: Box content { + orientation: vertical; + spacing: 6; + margin-start: 80; + margin-end: 80; + }; + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Backup View'; + }; + + valign: start; + + Button add_button { + label: 'Add Backup'; + + styles [ + "suggested-action", + ] + } + } +} \ No newline at end of file diff --git a/gui/src/gui/pinentry.blp b/gui/src/gui/pinentry.blp new file mode 100644 index 0000000..236d45c --- /dev/null +++ b/gui/src/gui/pinentry.blp @@ -0,0 +1,48 @@ +using Gtk 4.0; +using Adw 1; + +Adw.Window window { + default-width: 400; + default-height: 700; + + Adw.ToolbarView view { + content: Box { + orientation: vertical; + spacing: 6; + + Label message { + label: 'Placeholder label'; + } + + Entry password_entry { + placeholder-text: "Enter your password"; + visibility: false; + } + + Box button_box { + spacing: 6; + + Button cancel_button { + label: "Cancel"; + hexpand: true; + } + + Button approve_button { + label: "Approve"; + hexpand: true; + } + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Goldwarden Pinentry'; + }; + + valign: start; + } + } +} diff --git a/gui/src/gui/pinentry.py b/gui/src/gui/pinentry.py index a1abb0a..8cb8b8c 100644 --- a/gui/src/gui/pinentry.py +++ b/gui/src/gui/pinentry.py @@ -5,65 +5,45 @@ import gc import time from gi.repository import Gtk, Adw, GLib, Notify, Gdk from threading import Thread +from .template_loader import load_template import sys import os -message = sys.stdin.readline() - -class MyApp(Adw.Application): +class GoldwardenPinentryApp(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) def on_activate(self, app): - self.pinentry_window = MainWindow(application=app) - self.pinentry_window.present() - self.app = app + self.load() + self.window.present() -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def load(self): + builder = load_template("pinentry.ui") + self.window = builder.get_object("window") + self.message_label = builder.get_object("message") + self.message_label.set_label(self.message) + + self.cancel_button = builder.get_object("cancel_button") + self.cancel_button.connect("clicked", self.on_cancel_button_clicked) + self.approve_button = builder.get_object("approve_button") + self.approve_button.connect("clicked", self.on_approve_button_clicked) - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.set_child(self.stack) - - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - self.stack.add_child(box) - - label = Gtk.Label(label=message) - box.append(label) - - # Create the password entry - self.password_entry = Gtk.Entry() + self.password_entry = builder.get_object("password_entry") self.password_entry.set_placeholder_text("Enter your password") - self.password_entry.set_visibility(False) # Hide the password - box.append(self.password_entry) - # Create a button box for cancel and approve buttons - button_box = Gtk.Box(spacing=6) - box.append(button_box) + self.window.set_application(self) - # Cancel button - cancel_button = Gtk.Button(label="Cancel") - cancel_button.set_hexpand(True) # Make the button expand horizontally - def on_cancel_button_clicked(button): - print("", flush=True) - os._exit(0) - cancel_button.connect("clicked", on_cancel_button_clicked) - button_box.append(cancel_button) + def on_approve_button_clicked(self, button): + print(self.password_entry.get_text(), flush=True) + os._exit(0) - # Approve button - approve_button = Gtk.Button(label="Approve") - approve_button.set_hexpand(True) # Make the button expand horizontally - def on_approve_button_clicked(button): - print(self.password_entry.get_text(), flush=True) - os._exit(0) - approve_button.connect("clicked", on_approve_button_clicked) - button_box.append(approve_button) + def on_cancel_button_clicked(self, button): + print("", flush=True) + os._exit(0) - self.set_default_size(700, 200) - self.set_title("Goldwarden Pinentry") - -app = MyApp(application_id="com.quexten.Goldwarden.pinentry") -app.run(sys.argv) \ No newline at end of file +if __name__ == "__main__": + app = GoldwardenPinentryApp(application_id="com.quexten.Goldwarden.pinentry") + message = sys.stdin.readline() + app.message = message + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/pinentry_approval.blp b/gui/src/gui/pinentry_approval.blp new file mode 100644 index 0000000..96285c4 --- /dev/null +++ b/gui/src/gui/pinentry_approval.blp @@ -0,0 +1,43 @@ +using Gtk 4.0; +using Adw 1; + +Adw.Window window { + default-width: 400; + default-height: 200; + + Adw.ToolbarView view { + content: Box { + orientation: vertical; + spacing: 6; + + Label message { + label: 'Dynamic Message'; + } + + Box button_box { + spacing: 6; + + Button cancel_button { + label: "Cancel"; + hexpand: true; + } + + Button approve_button { + label: "Approve"; + hexpand: true; + } + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Goldwarden Approval'; + }; + + valign: start; + } + } +} \ No newline at end of file diff --git a/gui/src/gui/pinentry_approval.py b/gui/src/gui/pinentry_approval.py index 17bd2e2..0ecff53 100644 --- a/gui/src/gui/pinentry_approval.py +++ b/gui/src/gui/pinentry_approval.py @@ -5,59 +5,42 @@ import gc import time from gi.repository import Gtk, Adw, GLib, Notify, Gdk from threading import Thread +from .template_loader import load_template import sys import os -message = sys.stdin.readline() - -class MyApp(Adw.Application): +class GoldwardenPinentryApprovalApp(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) def on_activate(self, app): - self.pinentry_window = MainWindow(application=app) - self.pinentry_window.present() - self.app = app + self.load() + self.window.present() -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def load(self): + builder = load_template("pinentry_approval.ui") + self.window = builder.get_object("window") + self.message_label = builder.get_object("message") + self.message_label.set_label(self.message) + + self.cancel_button = builder.get_object("cancel_button") + self.cancel_button.connect("clicked", self.on_cancel_button_clicked) + self.approve_button = builder.get_object("approve_button") + self.approve_button.connect("clicked", self.on_approve_button_clicked) - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.set_child(self.stack) + self.window.set_application(self) - box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - self.stack.add_child(box) + def on_approve_button_clicked(self, button): + print("true", flush=True) + os._exit(0) - label = Gtk.Label(label=message) - box.append(label) + def on_cancel_button_clicked(self, button): + print("false", flush=True) + os._exit(0) - # Create a button box for cancel and approve buttons - button_box = Gtk.Box(spacing=6) - box.append(button_box) - - # Cancel button - cancel_button = Gtk.Button(label="Cancel") - cancel_button.set_hexpand(True) # Make the button expand horizontally - def on_cancel_button_clicked(button): - print("false", flush=True) - os._exit(0) - cancel_button.connect("clicked", on_cancel_button_clicked) - button_box.append(cancel_button) - - # Approve button - approve_button = Gtk.Button(label="Approve") - approve_button.set_hexpand(True) # Make the button expand horizontally - def on_approve_button_clicked(button): - print("true", flush=True) - os._exit(0) - approve_button.connect("clicked", on_approve_button_clicked) - button_box.append(approve_button) - - self.set_default_size(700, 200) - self.set_title("Goldwarden Approval") - -app = MyApp(application_id="com.quexten.Goldwarden.pinentry") -app.run(sys.argv) \ No newline at end of file +if __name__ == "__main__": + app = GoldwardenPinentryApprovalApp(application_id="com.quexten.Goldwarden.pinentry") + message = sys.stdin.readline() + app.message = message + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/quickaccess.blp b/gui/src/gui/quickaccess.blp new file mode 100644 index 0000000..688df1b --- /dev/null +++ b/gui/src/gui/quickaccess.blp @@ -0,0 +1,54 @@ +using Gtk 4.0; +using Adw 1; + + +Adw.Window window { + default-width: 400; + default-height: 700; + + Adw.ToolbarView view { + content: Box{ + orientation: vertical; + Box { + orientation: vertical; + + Adw.PreferencesPage preferences_page { + Adw.PreferencesGroup { + Adw.EntryRow search_row { + title: "Search"; + } + } + } + + Adw.StatusPage status_page { + visible: true; + margin-top: 100; + title: "Type to search"; + icon-name: "system-search-symbolic"; + } + + ListBox results_list { + margin-start: 10; + margin-end: 10; + margin-top: 10; + margin-bottom: 10; + visible: false; + styles [ + "boxed-list" + ] + } + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'QuickAccess'; + }; + + valign: start; + } + } +} \ No newline at end of file diff --git a/gui/src/gui/quickaccess.py b/gui/src/gui/quickaccess.py index f6bc976..9ed95e7 100644 --- a/gui/src/gui/quickaccess.py +++ b/gui/src/gui/quickaccess.py @@ -7,172 +7,189 @@ import time from gi.repository import Gtk, Adw, GLib, Notify, Gdk from ..services import goldwarden from threading import Thread +from .template_loader import load_template import sys import os from ..services import totp Notify.init("Goldwarden") -# read line from stdin -token = sys.stdin.readline() -goldwarden.create_authenticated_connection(token) +class GoldwardenQuickAccessApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.logins = [] + self.filtered_logins = [] + self.query = "" + self.connect('activate', self.on_activate) + self.selected_index = 0 -def autotype(text): - goldwarden.autotype(text) - time.sleep(0.1) - os._exit(0) + def on_activate(self, app): + self.load() + self.window.present() + thread = Thread(target=self.update_logins) + thread.start() -def set_clipboard(text): - Gdk.Display.get_clipboard(Gdk.Display.get_default()).set_content( + def load(self): + builder = load_template("quickaccess.ui") + self.window = builder.get_object("window") + self.results_list = builder.get_object("results_list") + self.status_page = builder.get_object("status_page") + self.text_view = builder.get_object("search_row") + self.text_view.connect("changed", self.on_type) + self.window.set_application(self) + + evk = Gtk.EventControllerKey.new() + evk.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + evk.connect("key-pressed", self.key_press) + self.window.add_controller(evk) + + def key_press(self, event, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: + os._exit(0) + + if keyval == Gdk.KEY_Tab: + return True + + if keyval == Gdk.KEY_Up: + self.selected_index = self.selected_index - 1 + if self.selected_index < 0: + self.selected_index = 0 + self.render_list() + return True + elif keyval == Gdk.KEY_Down: + self.selected_index = self.selected_index + 1 + if self.selected_index >= len(self.filtered_logins): + self.selected_index = len(self.filtered_logins) - 1 + self.render_list() + return True + + if self.selected_index >= len(self.filtered_logins) or self.selected_index < 0: + self.selected_index = 0 + + auto_type_combo = state & Gdk.ModifierType.CONTROL_MASK and state & Gdk.ModifierType.SHIFT_MASK + copy_combo = state & Gdk.ModifierType.CONTROL_MASK and not state & Gdk.ModifierType.SHIFT_MASK + + # totp code + if keyval == Gdk.KEY_t or keyval == Gdk.KEY_T: + if auto_type_combo: + self.autotype(totp.totp(self.filtered_logins[self.selected_index]["totp"])) + if copy_combo: + self.set_clipboard(totp.totp(self.filtered_logins[self.selected_index]["totp"])) + + if keyval == Gdk.KEY_u or keyval == Gdk.KEY_U: + if auto_type_combo: + self.autotype(self.filtered_logins[self.selected_index]["username"]) + if copy_combo: + self.set_clipboard(self.filtered_logins[self.selected_index]["username"]) + + if keyval == Gdk.KEY_p or keyval == Gdk.KEY_P: + if auto_type_combo: + self.autotype(self.filtered_logins[self.selected_index]["password"]) + if copy_combo: + self.set_clipboard(self.filtered_logins[self.selected_index]["password"]) + + if (keyval == Gdk.KEY_l or keyval == Gdk.KEY_L) and auto_type_combo: + Gtk.show_uri(None, self.results_list.get_selected_row().uri, Gdk.CURRENT_TIME) + + if (keyval == Gdk.KEY_v or keyval == Gdk.KEY_V) and auto_type_combo: + self.set_clipboard(self.filtered_logins[self.selected_index]["uri"]) + environment = goldwarden.get_environment() + if environment == None: + return + item_uri = environment["vault"] + "#/vault?itemId=" + self.results_list.get_selected_row().uuid + Gtk.show_uri(None, item_uri, Gdk.CURRENT_TIME) + + if keyval == Gdk.KEY_Return: + if auto_type_combo: + self.autotype(f"{self.filtered_logins[self.selected_index]['username']}\t{self.filtered_logins[self.selected_index]['password']}") + + def update(self): + self.update_list() + self.render_list() + return True + + def autotype(self, text): + def perform_autotype(text): + self.window.hide() + time.sleep(0.1) + goldwarden.autotype(text) + time.sleep(0.1) + os._exit(0) + thread = Thread(target=perform_autotype, args=(text,)) + thread.start() + + def set_clipboard(self, text): + Gdk.Display.get_clipboard(Gdk.Display.get_default()).set_content( Gdk.ContentProvider.new_for_value(text) ) - def kill(): - time.sleep(0.5) - os._exit(0) - thread = Thread(target=kill) - thread.start() + def kill(): + time.sleep(0.5) + os._exit(0) + thread = Thread(target=kill) + thread.start() -class MyApp(Adw.Application): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.connect('activate', self.on_activate) + def update_list(self): + if self.query == "": + self.filtered_logins = [] + return + + self.filtered_logins = list(filter(lambda i: self.query.lower() in i["name"].lower(), self.logins)) + self.starts_with_logins = list(filter(lambda i: i["name"].lower().startswith(self.query.lower()), self.filtered_logins)) + self.other_logins = list(filter(lambda i: i not in self.starts_with_logins, self.filtered_logins)) + self.filtered_logins = self.starts_with_logins + self.other_logins + if len(self.filtered_logins) > 7: + self.filtered_logins = self.filtered_logins[0:7] + + def render_list(self): + if len(self.filtered_logins) > 1: + self.results_list.set_visible(True) + while self.results_list.get_first_child() != None: + self.results_list.remove(self.results_list.get_first_child()) + self.status_page.set_visible(False) + else: + self.results_list.set_visible(False) + self.status_page.set_visible(True) + + for i in self.filtered_logins: + action_row = Adw.ActionRow() + action_row.set_title(i["name"]) + action_row.set_subtitle(i["username"]) + action_row.set_icon_name("dialog-password") + action_row.set_activatable(True) + action_row.password = i["password"] + action_row.username = i["username"] + action_row.uuid = i["uuid"] + action_row.uri = i["uri"] + action_row.totp = i["totp"] + self.results_list.append(action_row) + + # select the nth item + if len(self.filtered_logins) > 0: + self.results_list.select_row(self.results_list.get_row_at_index(self.selected_index)) + self.results_list.set_focus_child(self.results_list.get_row_at_index(self.selected_index)) + + self.starts_with_logins = None + self.other_logins = None + + def on_type(self, entry): + search_query = entry.get_text() + self.query = search_query + self.update() + def update_logins(self): logins = goldwarden.get_vault_logins() if logins == None: os._exit(0) return - self.app.autofill_window.logins = logins - - def on_activate(self, app): - self.autofill_window = MainWindow(application=app) - self.autofill_window.logins = [] - self.autofill_window.present() - self.app = app - thread = Thread(target=self.update_logins) - thread.start() - -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.set_child(self.stack) - - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.VERTICAL) - self.stack.add_named(self.box, "box") - - self.text_view = Adw.EntryRow() - self.text_view.set_title("Search") + self.logins = logins + self.update() - def on_type(entry): - if len(entry.get_text()) > 1: - self.results_list.show() - else: - self.results_list.hide() +if __name__ == "__main__": + settings = Gtk.Settings.get_default() + settings.set_property("gtk-error-bell", False) - while self.results_list.get_first_child() != None: - self.results_list.remove(self.results_list.get_first_child()) - - self.filtered_logins = list(filter(lambda i: entry.get_text().lower() in i["name"].lower(), self.logins)) - if len( self.filtered_logins) > 10: - self.filtered_logins = self.filtered_logins[0:10] - self.starts_with_logins = list(filter(lambda i: i["name"].lower().startswith(entry.get_text().lower()), self.logins)) - self.other_logins = list(filter(lambda i: i not in self.starts_with_logins , self.filtered_logins)) - self.filtered_logins = None - - for i in self.starts_with_logins + self.other_logins : - action_row = Adw.ActionRow() - action_row.set_title(i["name"]) - action_row.set_subtitle(i["username"]) - action_row.set_icon_name("dialog-password") - action_row.set_activatable(True) - action_row.password = i["password"] - action_row.username = i["username"] - action_row.uuid = i["uuid"] - action_row.uri = i["uri"] - action_row.totp = i["totp"] - self.results_list.append(action_row) - self.starts_with_logins = None - self.other_logins = None - self.text_view.connect("changed", lambda entry: on_type(entry)) - self.box.append(self.text_view) - - self.results_list = Gtk.ListBox() - # margin' - self.results_list.set_margin_start(10) - self.results_list.set_margin_end(10) - self.results_list.set_margin_top(10) - self.results_list.set_margin_bottom(10) - self.results_list.hide() - - keycont = Gtk.EventControllerKey() - def handle_keypress(controller, keyval, keycode, state, user_data): - ctrl_pressed = state & Gdk.ModifierType.CONTROL_MASK > 0 - alt_pressed = state & Gdk.ModifierType.ALT_MASK > 0 - - if keycode == 9: - os._exit(0) - - if keyval == 65364: - # focus results - if self.results_list.get_first_child() != None: - self.results_list.get_first_child().grab_focus() - self.results_list.select_row(self.results_list.get_first_child()) - - if keyval == 113: - return False - - if keycode == 36: - self.hide() - autotypeThread = Thread(target=autotype, args=(f"{self.results_list.get_selected_row().username}\t{self.results_list.get_selected_row().password}",)) - autotypeThread.start() - if keyval == 112: - print("pass", ctrl_pressed, alt_pressed) - if ctrl_pressed and not alt_pressed: - set_clipboard(self.results_list.get_selected_row().password) - if ctrl_pressed and alt_pressed: - self.hide() - autotypeThread = Thread(target=autotype, args=(self.results_list.get_selected_row().password,)) - autotypeThread.start() - elif keyval == 117: - if ctrl_pressed and not alt_pressed: - set_clipboard(self.results_list.get_selected_row().username) - if ctrl_pressed and alt_pressed: - self.hide() - autotypeThread = Thread(target=autotype, args=(self.results_list.get_selected_row().username,)) - autotypeThread.start() - elif keyval == 118: - if ctrl_pressed and alt_pressed: - environment = goldwarden.get_environment() - if environment == None: - return - item_uri = environment["vault"] + "#/vault?itemId=" + self.results_list.get_selected_row().uuid - Gtk.show_uri(None, item_uri, Gdk.CURRENT_TIME) - elif keyval == 108: - if ctrl_pressed and alt_pressed: - Gtk.show_uri(None, self.results_list.get_selected_row().uri, Gdk.CURRENT_TIME) - elif keyval == 116: - totp_code = totp.totp(self.results_list.get_selected_row().totp) - if ctrl_pressed and not alt_pressed: - set_clipboard(totp_code) - if ctrl_pressed and alt_pressed: - self.hide() - autotypeThread = Thread(target=autotype, args=(totp_code,)) - autotypeThread.start() - elif keyval == 102: - # focus search - self.text_view.grab_focus() - - keycont.connect('key-pressed', handle_keypress, self) - self.add_controller(keycont) - - self.results_list.get_style_context().add_class("boxed-list") - self.box.append(self.results_list) - self.set_default_size(700, 700) - self.set_title("Goldwarden Quick Access") - -app = MyApp(application_id="com.quexten.Goldwarden.autofill-menu") -app.run(sys.argv) + token = sys.stdin.readline() + goldwarden.create_authenticated_connection(token) + app = GoldwardenQuickAccessApp(application_id="com.quexten.Goldwarden.quickaccess") + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/settings.blp b/gui/src/gui/settings.blp new file mode 100644 index 0000000..ba2584a --- /dev/null +++ b/gui/src/gui/settings.blp @@ -0,0 +1,149 @@ +using Gtk 4.0; +using Adw 1; + + +Adw.ApplicationWindow window { + default-width: 400; + default-height: 700; + + Adw.ToolbarView view { + content: Box{ + orientation: vertical; + + Stack stack { + Box set_pin_status { + orientation: vertical; + Adw.StatusPage { + margin-top: 100; + title: "Pin required"; + icon-name: "dialog-password-symbolic"; + } + Button set_pin_button { + label: "Set pin"; + margin-start: 20; + margin-end: 20; + styles [ + "suggested-action", + "pill" + ] + } + } + + Box unlock_status { + orientation: vertical; + Adw.StatusPage { + margin-top: 100; + title: "Vault locked"; + icon-name: "security-high-symbolic"; + } + Button unlock_button { + label: "Unlock"; + margin-start: 20; + margin-end: 20; + styles [ + "suggested-action", + "pill" + ] + } + } + + Box login_status { + orientation: vertical; + Adw.StatusPage { + margin-top: 100; + title: "Logged out"; + icon-name: "system-users-symbolic"; + } + Button login_button { + label: "Log in"; + margin-start: 20; + margin-end: 20; + styles [ + "suggested-action", + "pill" + ] + } + } + + ScrolledWindow settings_view { + vexpand: true; + hexpand: true; + child: Box content { + orientation: vertical; + Adw.PreferencesPage preferences_page { + title: "General"; + Adw.PreferencesGroup { + title: "Actions"; + Button quickaccess_button { + label: "Quick Access"; + styles [ + "suggested-action" + ] + } + Button update_pin_button { + label: "Update pin"; + margin-top: 20; + styles [ + "suggested-action" + ] + } + Button lock_button { + label: "Lock"; + margin-top: 20; + styles [ + "suggested-action" + ] + } + Button logout_button { + label: "Logout"; + margin-top: 20; + styles [ + "destructive-action" + ] + } + } + Adw.PreferencesGroup { + title: "Vault Status"; + Adw.ActionRow last_sync_row { + title: "Last Sync"; + subtitle: "Never"; + icon-name: "emblem-synchronizing-symbolic"; + } + Adw.ActionRow websocket_connected_row { + title: "Websocked Connected"; + subtitle: "False"; + } + Adw.ActionRow logins_row { + title: "Vault Login Entries"; + subtitle: "0"; + icon-name: "dialog-password-symbolic"; + } + Adw.ActionRow notes_row { + title: "Vault Notes"; + subtitle: "0"; + icon-name: "emblem-documents-symbolic"; + } + } + } + }; + } + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Goldwarden'; + }; + + [end] + MenuButton menu_button { + icon-name: "open-menu-symbolic"; + } + + valign: start; + } + } +} diff --git a/gui/src/gui/settings.py b/gui/src/gui/settings.py index 71bab67..69ada13 100644 --- a/gui/src/gui/settings.py +++ b/gui/src/gui/settings.py @@ -1,322 +1,150 @@ #!/usr/bin/env python3 import sys import gi + gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -import gc from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ..services import goldwarden from threading import Thread +from .template_loader import load_template import subprocess -from . import components import os -root_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir)) -token = sys.stdin.readline() -goldwarden.create_authenticated_connection(None) - -def quickaccess_button_clicked(): - p = subprocess.Popen(["python3", "-m", "src.gui.quickaccess"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True) +def run_window(name, token): + gui_path = os.path.dirname(os.path.realpath(__file__)) + cwd = os.path.abspath(os.path.join(gui_path, os.pardir, os.pardir)) + print(f"Running window {name} with path {cwd}") + p = subprocess.Popen(["python3", "-m", "src.gui." + name], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd, start_new_session=True) p.stdin.write(f"{token}\n".encode()) p.stdin.flush() -def shortcuts_button_clicked(): - p = subprocess.Popen(["python3", "-m", "src.gui.shortcuts"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True) - p.stdin.write(f"{token}\n".encode()) - p.stdin.flush() -def ssh_button_clicked(): - p = subprocess.Popen(["python3", "-m", "src.gui.ssh"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True) - p.stdin.write(f"{token}\n".encode()) - p.stdin.flush() - -def browserbiometrics_button_clicked(): - p = subprocess.Popen(["python3", "-m", "src.gui.browserbiometrics"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=root_path, start_new_session=True) - p.stdin.write(f"{token}\n".encode()) - p.stdin.flush() - -def add_action_row(parent, title, subtitle, icon=None): - row = Adw.ActionRow() - row.set_title(title) - row.set_subtitle(subtitle) - if icon != None: - row.set_icon_name(icon) - parent.add(row) - return row - -class SettingsWinvdow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # vertical box - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.VERTICAL) - self.set_child(self.box) - - def set_pin(): - set_pin_thread = Thread(target=goldwarden.enable_pin) - set_pin_thread.start() - - self.banner = Adw.Banner() - self.banner.set_title("No pin set, please set it now") - self.banner.set_button_label("Set Pin") - self.banner.connect("button-clicked", lambda banner: set_pin()) - self.box.append(self.banner) - - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.box.append(self.stack) - - self.preferences_page = Adw.PreferencesPage() - self.preferences_page.set_title("General") - self.stack.add_named(self.preferences_page, "preferences_page") - - self.action_preferences_group = Adw.PreferencesGroup() - self.action_preferences_group.set_title("Actions") - self.preferences_page.add(self.action_preferences_group) - - self.autotype_button = Gtk.Button() - self.autotype_button.set_label("Quick Access") - self.autotype_button.set_margin_top(10) - - self.autotype_button.connect("clicked", lambda button: quickaccess_button_clicked()) - self.autotype_button.get_style_context().add_class("suggested-action") - self.action_preferences_group.add(self.autotype_button) - - self.login_button = Gtk.Button() - self.login_button.set_label("Login") - self.login_button.connect("clicked", lambda button: show_login()) - self.login_button.set_sensitive(False) - self.login_button.set_margin_top(10) - self.login_button.get_style_context().add_class("suggested-action") - self.action_preferences_group.add(self.login_button) - - self.set_pin_button = Gtk.Button() - self.set_pin_button.set_label("Set Pin") - self.set_pin_button.connect("clicked", lambda button: set_pin()) - self.set_pin_button.set_margin_top(10) - self.set_pin_button.set_sensitive(False) - self.set_pin_button.get_style_context().add_class("suggested-action") - self.action_preferences_group.add(self.set_pin_button) - - self.unlock_button = Gtk.Button() - self.unlock_button.set_label("Unlock") - self.unlock_button.set_margin_top(10) - def unlock_button_clicked(): - action = goldwarden.unlock if self.unlock_button.get_label() == "Unlock" else goldwarden.lock - unlock_thread = Thread(target=action) - unlock_thread.start() - self.unlock_button.connect("clicked", lambda button: unlock_button_clicked()) - # set disabled - self.unlock_button.set_sensitive(False) - self.action_preferences_group.add(self.unlock_button) - - self.logout_button = Gtk.Button() - self.logout_button.set_label("Logout") - self.logout_button.set_margin_top(10) - self.logout_button.connect("clicked", lambda button: goldwarden.purge()) - self.logout_button.get_style_context().add_class("destructive-action") - self.action_preferences_group.add(self.logout_button) - - self.wiki_button = Gtk.LinkButton(uri="https://github.com/quexten/goldwarden/wiki/Flatpak-Configuration") - self.wiki_button.set_label("Help & Wiki") - self.wiki_button.set_margin_top(10) - self.action_preferences_group.add(self.wiki_button) - - self.vault_status_preferences_group = Adw.PreferencesGroup() - self.vault_status_preferences_group.set_title("Vault Status") - self.preferences_page.add(self.vault_status_preferences_group) - - self.status_row = add_action_row(self.vault_status_preferences_group, "Vault Status", "Locked") - - self.vault_status_icon = components.StatusIcon() - self.vault_status_icon.set_icon("dialog-error", "error") - self.status_row.add_prefix(self.vault_status_icon) - - self.last_sync_row = add_action_row(self.vault_status_preferences_group, "Last Sync", "Never", "emblem-synchronizing-symbolic") - self.websocket_connected_row = add_action_row(self.vault_status_preferences_group, "Websocket Connected", "False") - - self.websocket_connected_status_icon = components.StatusIcon() - self.websocket_connected_status_icon.set_icon("dialog-error", "error") - self.websocket_connected_row.add_prefix(self.websocket_connected_status_icon) - - self.login_row = add_action_row(self.vault_status_preferences_group, "Vault Login Entries", "0", "dialog-password-symbolic") - self.notes_row = add_action_row(self.vault_status_preferences_group, "Vault Notes", "0", "emblem-documents-symbolic") - - self.header = Gtk.HeaderBar() - self.set_titlebar(self.header) - - action = Gio.SimpleAction.new("shortcuts", None) - action.connect("activate", lambda action, parameter: shortcuts_button_clicked()) - self.add_action(action) - menu = Gio.Menu.new() - menu.append("Keyboard Shortcuts", "win.shortcuts") - self.popover = Gtk.PopoverMenu() - self.popover.set_menu_model(menu) - - action = Gio.SimpleAction.new("ssh", None) - action.connect("activate", lambda action, parameter: ssh_button_clicked()) - self.add_action(action) - menu.append("SSH Agent", "win.ssh") - - action = Gio.SimpleAction.new("browserbiometrics", None) - action.connect("activate", lambda action, parameter: browserbiometrics_button_clicked()) - self.add_action(action) - menu.append("Browser Biometrics", "win.browserbiometrics") - - self.hamburger = Gtk.MenuButton() - self.hamburger.set_popover(self.popover) - self.hamburger.set_icon_name("open-menu-symbolic") - self.header.pack_start(self.hamburger) - - - def update_labels(): - pin_set = goldwarden.is_pin_enabled() - status = goldwarden.get_vault_status() - print("status", status) - runtimeCfg = goldwarden.get_runtime_config() - - if status != None: - if pin_set: - self.unlock_button.set_sensitive(True) - self.banner.set_revealed(False) - else: - self.unlock_button.set_sensitive(False) - self.banner.set_revealed(True) - logged_in = status["loggedIn"] - if logged_in and not status["locked"]: - self.autotype_button.set_visible(True) - self.login_row.set_sensitive(True) - self.notes_row.set_sensitive(True) - self.websocket_connected_row.set_sensitive(True) - else: - self.autotype_button.set_visible(False) - self.websocket_connected_row.set_sensitive(False) - self.login_row.set_sensitive(False) - self.notes_row.set_sensitive(False) - - locked = status["locked"] - self.login_button.set_sensitive(pin_set and not locked) - self.set_pin_button.set_sensitive(not pin_set or not locked) - self.autotype_button.set_sensitive(not locked) - self.status_row.set_subtitle(str("Logged in" if (logged_in and not locked) else "Logged out") if not locked else "Locked") - if locked or not logged_in: - self.vault_status_icon.set_icon("dialog-warning", "warning") - else: - self.vault_status_icon.set_icon("emblem-default", "ok") - if not logged_in: - self.logout_button.set_sensitive(False) - else: - self.logout_button.set_sensitive(True) - self.login_row.set_subtitle(str(status["loginEntries"])) - self.notes_row.set_subtitle(str(status["noteEntries"])) - self.websocket_connected_row.set_subtitle("Connected" if status["websocketConnected"] else "Disconnected") - if status["websocketConnected"]: - self.websocket_connected_status_icon.set_icon("emblem-default", "ok") - else: - self.websocket_connected_status_icon.set_icon("dialog-error", "error") - self.last_sync_row.set_subtitle(str(status["lastSynced"])) - if status["lastSynced"].startswith("1970") or status["lastSynced"].startswith("1969"): - self.last_sync_row.set_subtitle("Never") - self.unlock_button.set_label("Unlock" if locked else "Lock") - else: - is_daemon_running = goldwarden.is_daemon_running() - if not is_daemon_running: - self.status_row.set_subtitle("Daemon not running") - self.vault_status_icon.set_icon("dialog-error", "error") - - GLib.timeout_add(5000, update_labels) - - GLib.timeout_add(1000, update_labels) - self.set_default_size(400, 700) - self.set_title("Goldwarden") - -class MyApp(Adw.Application): +class GoldwardenSettingsApp(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) def on_activate(self, app): - self.settings_win = SettingsWinvdow(application=app) - self.settings_win.present() + self.load() + self.update() + self.window.present() + GLib.timeout_add(100, self.update) -def show_login(): - dialog = Gtk.Dialog(title="Goldwarden") + def load(self): + builder = load_template("settings.ui") + self.window = builder.get_object("window") + self.window.set_application(self) + self.stack = builder.get_object("stack") - auth_preference_group = Adw.PreferencesGroup() - auth_preference_group.set_title("Authentication") - auth_preference_group.set_margin_top(10) - auth_preference_group.set_margin_bottom(10) - auth_preference_group.set_margin_start(10) - auth_preference_group.set_margin_end(10) - dialog.get_content_area().append(auth_preference_group) + self.set_pin_status_box = builder.get_object("set_pin_status") + self.set_pin_button = builder.get_object("set_pin_button") + self.set_pin_button.connect("clicked", lambda x: goldwarden.enable_pin()) - email_entry = Adw.EntryRow() - email_entry.set_title("Email") - email_entry.set_text("") - auth_preference_group.add(email_entry) + self.unlock_status_box = builder.get_object("unlock_status") + self.unlock_button = builder.get_object("unlock_button") + self.unlock_button.connect("clicked", lambda x: goldwarden.unlock()) + + self.login_status_box = builder.get_object("login_status") + self.login_button = builder.get_object("login_button") + self.login_button.connect("clicked", lambda x: run_window("login", self.token)) - client_id_entry = Adw.EntryRow() - client_id_entry.set_title("Client ID (optional)") - client_id_entry.set_text("") - auth_preference_group.add(client_id_entry) + self.settings_view = builder.get_object("settings_view") + self.lock_button = builder.get_object("lock_button") + self.lock_button.connect("clicked", lambda x: goldwarden.lock()) + self.logout_button = builder.get_object("logout_button") + self.logout_button.connect("clicked", lambda x: goldwarden.purge()) + self.update_pin_button = builder.get_object("update_pin_button") + self.update_pin_button.connect("clicked", lambda x: goldwarden.enable_pin()) + self.quickaccess_button = builder.get_object("quickaccess_button") + self.quickaccess_button.connect("clicked", lambda x: run_window("quickaccess", self.token)) + self.last_sync_row = builder.get_object("last_sync_row") + self.websocket_connected_row = builder.get_object("websocket_connected_row") + self.logins_row = builder.get_object("logins_row") + self.notes_row = builder.get_object("notes_row") - client_secret_entry = Adw.EntryRow() - client_secret_entry.set_title("Client Secret (optional)") - client_secret_entry.set_text("") - auth_preference_group.add(client_secret_entry) + self.menu_button = builder.get_object("menu_button") + menu = Gio.Menu.new() + self.popover = Gtk.PopoverMenu() + self.popover.set_menu_model(menu) + self.menu_button.set_popover(self.popover) - dialog.add_button("Login", Gtk.ResponseType.OK) - def on_save(res): - if res != Gtk.ResponseType.OK: + action = Gio.SimpleAction.new("shortcuts", None) + action.connect("activate", lambda action, parameter: run_window("shortcuts", self.token)) + self.window.add_action(action) + menu.append("Keyboard Shortcuts", "win.shortcuts") + + action = Gio.SimpleAction.new("ssh", None) + action.connect("activate", lambda action, parameter: run_window("ssh", self.token)) + self.window.add_action(action) + menu.append("SSH Agent", "win.ssh") + + action = Gio.SimpleAction.new("browserbiometrics", None) + action.connect("activate", lambda action, parameter: run_window("browserbiometrics", self.token)) + self.window.add_action(action) + menu.append("Browser Biometrics", "win.browserbiometrics") + + action = Gio.SimpleAction.new("about", None) + action.connect("activate", lambda action, parameter: self.show_about()) + self.window.add_action(action) + menu.append("About", "win.about") + + def update(self): + self.render() + return True + + def render(self): + pin_set = goldwarden.is_pin_enabled() + status = goldwarden.get_vault_status() + runtimeCfg = goldwarden.get_runtime_config() + if status == None: + is_daemon_running = goldwarden.is_daemon_running() + if not is_daemon_running: + self.status_row.set_subtitle("Daemon not running") + self.vault_status_icon.set_icon("dialog-error", "error") return - goldwarden.set_url(url_entry.get_text()) - goldwarden.set_client_id(client_id_entry.get_text()) - goldwarden.set_client_secret(client_secret_entry.get_text()) - def login(): - res = goldwarden.login_with_password(email_entry.get_text(), "password") - def handle_res(): - if res == "ok": - dialog.close() - # elif res == "badpass": - # bad_pass_diag = Gtk.MessageDialog(transient_for=dialog, modal=True, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text="Bad password") - # bad_pass_diag.connect("response", lambda dialog, response: bad_pass_diag.close()) - # bad_pass_diag.present() - GLib.idle_add(handle_res) - login_thread = Thread(target=login) - login_thread.start() + logged_in = status["loggedIn"] + unlocked = not status["locked"] + if not pin_set: + self.stack.set_visible_child(self.set_pin_status_box) + return + if not unlocked: + self.stack.set_visible_child(self.unlock_status_box) + return + if not logged_in: + self.stack.set_visible_child(self.login_status_box) + return + self.stack.set_visible_child(self.settings_view) - preference_group = Adw.PreferencesGroup() - preference_group.set_title("Config") - preference_group.set_margin_top(10) - preference_group.set_margin_bottom(10) - preference_group.set_margin_start(10) - preference_group.set_margin_end(10) + self.last_sync_row.set_subtitle(status["lastSynced"]) + self.websocket_connected_row.set_subtitle("Yes" if status["websocketConnected"] else "No") + self.logins_row.set_subtitle(str(status["loginEntries"])) + self.notes_row.set_subtitle(str(status["noteEntries"])) - dialog.get_content_area().append(preference_group) + def show_about(self): + dialog = Adw.AboutWindow(transient_for=app.get_active_window()) + dialog.set_application_name("Goldwarden") + dialog.set_version("dev") + dialog.set_developer_name("Bernd Schoolmann (Quexten)") + dialog.set_license_type(Gtk.License(Gtk.License.MIT_X11)) + dialog.set_comments("A Bitwarden compatible password manager") + dialog.set_website("https://github.com/quexten/goldwarden") + dialog.set_issue_url("https://github.com/quexten/goldwarden/issues") + dialog.add_credit_section("Contributors", ["Bernd Schoolmann"]) + dialog.set_copyright("© 2024 Bernd Schoolmann") + dialog.set_developers(["Bernd Schoolmann"]) + dialog.set_application_icon("com.quexten.Goldwarden") + dialog.set_visible(True) - url_entry = Adw.EntryRow() - url_entry.set_title("Base Url") - url_entry.set_text("https://vault.bitwarden.com/") - preference_group.add(url_entry) +if __name__ == "__main__": + settings = Gtk.Settings.get_default() + settings.set_property("gtk-error-bell", False) - #ok response - dialog.connect("response", lambda dialog, response: on_save(response)) - dialog.set_default_size(400, 200) - dialog.set_modal(True) - dialog.present() + token = sys.stdin.readline().strip() -isflatpak = os.path.exists("/.flatpak-info") -pathprefix = "/app/bin/" if isflatpak else "./" -css_provider = Gtk.CssProvider() -css_provider.load_from_path(pathprefix+"style.css") -Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION -) - -app = MyApp(application_id="com.quexten.Goldwarden.settings") -app.run(sys.argv) \ No newline at end of file + goldwarden.create_authenticated_connection(None) + app = GoldwardenSettingsApp(application_id="com.quexten.Goldwarden.settings") + app.token = token + app.run(sys.argv) diff --git a/gui/src/gui/shortcuts.blp b/gui/src/gui/shortcuts.blp new file mode 100644 index 0000000..3c3d53f --- /dev/null +++ b/gui/src/gui/shortcuts.blp @@ -0,0 +1,91 @@ +using Gtk 4.0; +using Adw 1; + +Adw.Window window { + default-width: 400; + default-height: 700; + + Adw.ToolbarView view { + content: Box{ + orientation: vertical; + Adw.Banner pin_banner { + title: 'No pin set, please set it now'; + button-label: 'Set Pin'; + revealed: false; + } + + ScrolledWindow { + vexpand: true; + hexpand: true; + child: Box content { + orientation: vertical; + Adw.PreferencesPage preferences_page { + title: "General"; + + Adw.PreferencesGroup global_preferences_group { + title: "Global Shortcuts"; + + Adw.ActionRow autofill_row { + title: "Autofill Shortcut"; + subtitle: "Not implemented - check the wiki for manual setup"; + } + } + + Adw.PreferencesGroup quick_access_preferences_group { + title: "Quick Access Shortcuts"; + Adw.ActionRow { + title: "Copy Username Shortcut"; + subtitle: "CTRL + U"; + } + Adw.ActionRow { + title: "Autotype Username Shortcut"; + subtitle: "CTRL + SHIFT + U"; + } + Adw.ActionRow { + title: "Copy Password Shortcut"; + subtitle: "CTRL + P"; + } + Adw.ActionRow { + title: "Autotype Password Shortcut"; + subtitle: "CTRL + SHIFT + P"; + } + Adw.ActionRow { + title: "Copy TOTP Shortcut"; + subtitle: "CTRL + T"; + } + Adw.ActionRow { + title: "Autotype TOTP Shortcut"; + subtitle: "CTRL + SHIFT + T"; + } + Adw.ActionRow { + title: "Launch URI Shortcut"; + subtitle: "CTRL + SHIFT + L"; + } + Adw.ActionRow { + title: "Launch Web Vault Shortcut"; + subtitle: "CTRL + SHIFT + V"; + } + Adw.ActionRow { + title: "Quit Shortcut"; + subtitle: "Esc"; + } + } + } + }; + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'Settings'; + }; + + valign: start; + } + } +} + + diff --git a/gui/src/gui/shortcuts.py b/gui/src/gui/shortcuts.py index 1cf4e6a..68b824f 100644 --- a/gui/src/gui/shortcuts.py +++ b/gui/src/gui/shortcuts.py @@ -1,80 +1,32 @@ +#!/usr/bin/env python3 +import sys import gi + gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -import gc -import time -from gi.repository import Gtk, Adw, GLib, Notify, Gdk + +from gi.repository import Gtk, Adw, GLib, Gdk, Gio +from ..services import goldwarden from threading import Thread -import sys -import os +from .template_loader import load_template +import subprocess from . import components +import os -def add_action_row(parent, title, subtitle, icon=None): - row = Adw.ActionRow() - row.set_title(title) - row.set_subtitle(subtitle) - if icon != None: - row.set_icon_name(icon) - parent.add(row) - return row - -class MyApp(Adw.Application): +class GoldwardenShortcutsSetupGuideApp(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) def on_activate(self, app): - self.pinentry_window = MainWindow(application=app) - self.pinentry_window.present() - self.app = app + self.load() + self.window.present() -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def load(self): + builder = load_template("shortcuts.ui") + self.window = builder.get_object("window") + self.window.set_application(self) - # vertical box - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.VERTICAL) - self.set_child(self.box) - - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.box.append(self.stack) - - self.preferences_page = Adw.PreferencesPage() - self.preferences_page.set_title("General") - self.stack.add_named(self.preferences_page, "preferences_page") - - self.global_preferences_group = Adw.PreferencesGroup() - self.global_preferences_group.set_title("Global Shortcuts") - self.preferences_page.add(self.global_preferences_group) - - self.autofill_row = Adw.ActionRow() - self.autofill_row.set_title("Autofill Shortcut") - self.autofill_row.set_subtitle("Not implemented - check the wiki for manual setup") - self.global_preferences_group.add(self.autofill_row) - - self.autofill_icon = components.StatusIcon() - self.autofill_icon.set_icon("dialog-warning", "warning") - self.autofill_row.add_prefix(self.autofill_icon) - - self.quickaccess_preferences_group = Adw.PreferencesGroup() - self.quickaccess_preferences_group.set_title("Quick Access Shortcuts") - self.preferences_page.add(self.quickaccess_preferences_group) - - add_action_row(self.quickaccess_preferences_group, "Copy Username Shortcut", "CTRL + U") - add_action_row(self.quickaccess_preferences_group, "Autotype Username Shortcut", "CTRL + ALT + U") - add_action_row(self.quickaccess_preferences_group, "Copy Password Shortcut", "CTRL + P") - add_action_row(self.quickaccess_preferences_group, "Autotype Password Shortcut", "CTRL + ALT + P") - add_action_row(self.quickaccess_preferences_group, "Copy TOTP Shortcut", "CTRL + T") - add_action_row(self.quickaccess_preferences_group, "Autotype TOTP Shortcut", "CTRL + ALT + T") - add_action_row(self.quickaccess_preferences_group, "Launch URI Shortcut", "CTRL+L") - add_action_row(self.quickaccess_preferences_group, "Launch Web Vault Shortcut", "CTRL+V") - add_action_row(self.quickaccess_preferences_group, "Focus Search Shortcut", "F") - add_action_row(self.quickaccess_preferences_group, "Quit Shortcut", "Esc") - - self.set_default_size(700, 700) - self.set_title("Goldwarden Shortcuts") - -app = MyApp(application_id="com.quexten.Goldwarden.shortcuts") -app.run(sys.argv) \ No newline at end of file +if __name__ == "__main__": + app = GoldwardenShortcutsSetupGuideApp(application_id="com.quexten.Goldwarden.shortcuts") + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/ssh.blp b/gui/src/gui/ssh.blp new file mode 100644 index 0000000..9233813 --- /dev/null +++ b/gui/src/gui/ssh.blp @@ -0,0 +1,57 @@ +using Gtk 4.0; +using Adw 1; + + +Adw.Window window { + default-width: 400; + default-height: 700; + + Adw.ToolbarView view { + content: Box{ + orientation: vertical; + ScrolledWindow { + vexpand: true; + hexpand: true; + child: Box content { + orientation: vertical; + Adw.PreferencesPage { + title: "General"; + + Adw.PreferencesGroup add_ssh_key_group { + title: "Add an SSH Key"; + Adw.ActionRow add_ssh_key_row { + subtitle: "flatpak run --command=goldwarden com.quexten.Goldwarden ssh add --name MY_KEY_NAME"; + subtitle-selectable: true; + } + } + + Adw.PreferencesGroup ssh_socket_path_group { + title: "SSH Socket Path"; + description: "Add this to your environment variables"; + Adw.ActionRow ssh_socket_path_row { + subtitle: "export SSH_AUTH_SOCK=/home/$USER/.var/app/com.quexten.Goldwarden/data/ssh-auth-sock"; + subtitle-selectable: true; + } + } + + Adw.PreferencesGroup git_signing_group { + title: "Git Signing"; + description: "Check the wiki for more information"; + } + } + }; + } + }; + + [top] + Adw.HeaderBar { + halign: baseline; + + title-widget: Adw.WindowTitle { + title: 'SSH Configuration'; + }; + + valign: start; + } + } +} \ No newline at end of file diff --git a/gui/src/gui/ssh.py b/gui/src/gui/ssh.py index a76a3ac..3681997 100644 --- a/gui/src/gui/ssh.py +++ b/gui/src/gui/ssh.py @@ -1,67 +1,32 @@ +#!/usr/bin/env python3 +import sys import gi + gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -import gc -import time -from gi.repository import Gtk, Adw, GLib, Notify, Gdk -from threading import Thread -import sys -import os -from . import components -class MyApp(Adw.Application): +from gi.repository import Gtk, Adw, GLib, Gdk, Gio +from ..services import goldwarden +from threading import Thread +from .template_loader import load_template +import subprocess +from . import components +import os + +class GoldwardenSSHSetupGuideApp(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) self.connect('activate', self.on_activate) def on_activate(self, app): - self.pinentry_window = MainWindow(application=app) - self.pinentry_window.present() - self.app = app + self.load() + self.window.present() -class MainWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def load(self): + builder = load_template("ssh.ui") + self.window = builder.get_object("window") + self.window.set_application(self) - # vertical box - self.box = Gtk.Box() - self.box.set_orientation(Gtk.Orientation.VERTICAL) - self.set_child(self.box) - - self.stack = Gtk.Stack() - self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.box.append(self.stack) - - self.preferences_page = Adw.PreferencesPage() - self.preferences_page.set_title("General") - self.stack.add_named(self.preferences_page, "preferences_page") - - self.add_ssh_key_group = Adw.PreferencesGroup() - self.add_ssh_key_group.set_title("Add an SSH Key") - self.preferences_page.add(self.add_ssh_key_group) - - self.add_ssh_key_row = Adw.ActionRow() - self.add_ssh_key_row.set_subtitle("flatpak run --command=goldwarden com.quexten.Goldwarden ssh add --name MY_KEY_NAME") - self.add_ssh_key_row.set_subtitle_selectable(True) - self.add_ssh_key_group.add(self.add_ssh_key_row) - - self.ssh_socket_path_group = Adw.PreferencesGroup() - self.ssh_socket_path_group.set_title("SSH Socket Path") - self.ssh_socket_path_group.set_description("Add this to your environment variables") - self.preferences_page.add(self.ssh_socket_path_group) - - self.ssh_socket_path_row = Adw.ActionRow() - self.ssh_socket_path_row.set_subtitle("export SSH_AUTH_SOCK=/home/$USER/.var/app/com.quexten.Goldwarden/data/ssh-auth-sock") - self.ssh_socket_path_row.set_subtitle_selectable(True) - self.ssh_socket_path_group.add(self.ssh_socket_path_row) - - self.git_signing_group = Adw.PreferencesGroup() - self.git_signing_group.set_title("Git Signing") - self.git_signing_group.set_description("Check the wiki for more information") - self.preferences_page.add(self.git_signing_group) - - self.set_default_size(400, 700) - self.set_title("Goldwarden SSH Setup") - -app = MyApp(application_id="com.quexten.Goldwarden.sshsetup") -app.run(sys.argv) \ No newline at end of file +if __name__ == "__main__": + app = GoldwardenSSHSetupGuideApp(application_id="com.quexten.Goldwarden.sshsetup") + app.run(sys.argv) \ No newline at end of file diff --git a/gui/src/gui/template_loader.py b/gui/src/gui/template_loader.py new file mode 100644 index 0000000..65c0a8c --- /dev/null +++ b/gui/src/gui/template_loader.py @@ -0,0 +1,10 @@ +import os +from gi.repository import Gtk + +isflatpak = os.path.exists("/.flatpak-info") +pathprefix = "/app/bin/src/gui/" if isflatpak else "./src/gui/" + +def load_template(path): + builder = Gtk.Builder() + builder.add_from_file(pathprefix + ".templates/" + path) + return builder diff --git a/gui/src/services/totp.py b/gui/src/services/totp.py index ff3fa5b..9893bd8 100644 --- a/gui/src/services/totp.py +++ b/gui/src/services/totp.py @@ -17,4 +17,8 @@ def hotp(key, counter, digits=6, digest='sha1'): def totp(key, time_step=30, digits=6, digest='sha1'): + if key.startswith('otpauth://'): + key = key.split('secret=')[1].split('&')[0] + key = key.replace(' ', '') + key = key.strip() return hotp(key, int(time.time() / time_step), digits, digest) \ No newline at end of file