From 6f30d75b081509179dc994ea16c08fce1c3062de Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 23 Dec 2023 07:18:30 +0100 Subject: [PATCH] Add prototype GTK UI --- .gitignore | 3 +- Readme.md | 4 +- agent/actions/logins.go | 12 +- autofill/autofill.go | 78 ---- autofill/autotype/uinput/dvorak.go | 384 ----------------- autofill/autotype/uinput/qwerty.go | 387 ------------------ autofill/autotype/uinput/uinput.go | 175 -------- autofill/autotype/uinputautotype.go | 9 - autofill/autotype/unimplemented.go | 7 - autofill/gioAutofillDialog.go | 336 --------------- .../libportalautotype.go | 4 +- cmd/autofill.go | 19 +- cmd/logins.go | 61 +++ cmd/vault.go | 8 +- go.mod | 3 + go.sum | 6 + ui/autotype/autotype.py | 101 +++++ ui/clipboard.py | 5 + ui/goldwarden.py | 87 ++++ ui/main.py | 286 +++++++++++++ ui/monitors/dbus_autofill_monitor.py | 23 ++ ui/monitors/global_shortcut_portal.py | 1 + 22 files changed, 605 insertions(+), 1394 deletions(-) delete mode 100644 autofill/autofill.go delete mode 100644 autofill/autotype/uinput/dvorak.go delete mode 100644 autofill/autotype/uinput/qwerty.go delete mode 100644 autofill/autotype/uinput/uinput.go delete mode 100644 autofill/autotype/uinputautotype.go delete mode 100644 autofill/autotype/unimplemented.go delete mode 100644 autofill/gioAutofillDialog.go rename {autofill/autotype => autotype}/libportalautotype.go (96%) create mode 100644 ui/autotype/autotype.py create mode 100644 ui/clipboard.py create mode 100644 ui/goldwarden.py create mode 100644 ui/main.py create mode 100644 ui/monitors/dbus_autofill_monitor.py create mode 100644 ui/monitors/global_shortcut_portal.py diff --git a/.gitignore b/.gitignore index 034f749..db73830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode -goldwarden* \ No newline at end of file +goldwarden +__pycache__ \ No newline at end of file diff --git a/Readme.md b/Readme.md index 9b821e4..35c6e65 100644 --- a/Readme.md +++ b/Readme.md @@ -85,9 +85,9 @@ Run a command with injected environment variables goldwarden run -- ``` -Autofill +Autofill (Flatpak only?) ``` -goldwarden autofill --layout +dbus-send --type=method_call --dest=com.quexten.goldwarden /com/quexten/goldwarden com.quexten.goldwarden.Autofill.autofill ``` (Create a hotkey for this depending on your desktop environment) diff --git a/agent/actions/logins.go b/agent/actions/logins.go index 0c6b52b..a53a662 100644 --- a/agent/actions/logins.go +++ b/agent/actions/logins.go @@ -101,7 +101,7 @@ func handleGetLoginCipher(request messages.IPCMessage, cfg *config.Config, vault } func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx *sockets.CallingContext) (response messages.IPCMessage, err error) { - if approved, err := pinentry.GetApproval("Approve List Credentials", fmt.Sprintf("%s on %s>%s>%s is trying to list credentials (name & username)", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved { + if approved, err := pinentry.GetApproval("Approve List Credentials", fmt.Sprintf("%s on %s>%s>%s is trying access all credentials", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved { response, err = messages.IPCMessageFromPayload(messages.ActionResponse{ Success: false, Message: "not approved", @@ -123,6 +123,7 @@ func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, va var decryptedName []byte = []byte{} var decryptedUsername []byte = []byte{} + var decryptedPassword []byte = []byte{} if !login.Name.IsNull() { decryptedName, err = crypto.DecryptWith(login.Name, key) @@ -140,10 +141,19 @@ func handleListLoginsRequest(request messages.IPCMessage, cfg *config.Config, va } } + if !login.Login.Password.IsNull() { + decryptedPassword, err = crypto.DecryptWith(login.Login.Password, key) + if err != nil { + actionsLog.Warn("Could not decrypt login:" + err.Error()) + continue + } + } + decryptedLoginCiphers = append(decryptedLoginCiphers, messages.DecryptedLoginCipher{ Name: string(decryptedName), Username: string(decryptedUsername), UUID: login.ID.String(), + Password: string(decryptedPassword), }) // prevent deadlock from enclaves diff --git a/autofill/autofill.go b/autofill/autofill.go deleted file mode 100644 index 527ed14..0000000 --- a/autofill/autofill.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build !noautofill - -package autofill - -import ( - "errors" - - "github.com/atotto/clipboard" - "github.com/quexten/goldwarden/autofill/autotype" - "github.com/quexten/goldwarden/client" - "github.com/quexten/goldwarden/ipc/messages" -) - -func GetLoginByUUID(uuid string, client client.Client) (messages.DecryptedLoginCipher, error) { - resp, err := client.SendToAgent(messages.GetLoginRequest{ - UUID: uuid, - }) - if err != nil { - return messages.DecryptedLoginCipher{}, err - } - - switch resp.(type) { - case messages.GetLoginResponse: - castedResponse := (resp.(messages.GetLoginResponse)) - return castedResponse.Result, nil - case messages.ActionResponse: - castedResponse := (resp.(messages.ActionResponse)) - return messages.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) - default: - return messages.DecryptedLoginCipher{}, errors.New("Wrong response type") - } -} - -func ListLogins(client client.Client) ([]messages.DecryptedLoginCipher, error) { - resp, err := client.SendToAgent(messages.ListLoginsRequest{}) - if err != nil { - return []messages.DecryptedLoginCipher{}, err - } - - switch resp.(type) { - case messages.GetLoginsResponse: - castedResponse := (resp.(messages.GetLoginsResponse)) - return castedResponse.Result, nil - case messages.ActionResponse: - castedResponse := (resp.(messages.ActionResponse)) - return []messages.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) - default: - return []messages.DecryptedLoginCipher{}, errors.New("Wrong response type") - } -} - -func Run(layout string, client client.Client) { - logins, err := ListLogins(client) - if err != nil { - panic(err) - } - - autofillEntries := []AutofillEntry{} - for _, login := range logins { - autofillEntries = append(autofillEntries, AutofillEntry{ - Name: login.Name, - Username: login.Username, - UUID: login.UUID, - }) - } - - RunAutofill(autofillEntries, func(uuid string, c chan bool) { - login, err := GetLoginByUUID(uuid, client) - if err != nil { - panic(err) - } - - autotype.TypeString(string(login.Username)+"\t"+string(login.Password), layout) - - clipboard.WriteAll(login.TwoFactorCode) - c <- true - }) -} diff --git a/autofill/autotype/uinput/dvorak.go b/autofill/autotype/uinput/dvorak.go deleted file mode 100644 index 7dbf68f..0000000 --- a/autofill/autotype/uinput/dvorak.go +++ /dev/null @@ -1,384 +0,0 @@ -package uinput - -import ( - "errors" - "fmt" - - "github.com/bendahl/uinput" -) - -type Dvorak struct { -} - -func (d Dvorak) TypeKey(key Key, keyboard uinput.Keyboard) error { - var err error - switch key { - case KeyA: - err = keyboard.KeyPress(uinput.KeyA) - break - case KeyAUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyA) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyB: - err = keyboard.KeyPress(uinput.KeyN) - break - case KeyBUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyN) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyC: - err = keyboard.KeyPress(uinput.KeyI) - break - case KeyCUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyI) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyD: - err = keyboard.KeyPress(uinput.KeyH) - break - case KeyDUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyH) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyE: - err = keyboard.KeyPress(uinput.KeyD) - break - case KeyEUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - err = keyboard.KeyPress(uinput.KeyD) - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyF: - err = keyboard.KeyPress(uinput.KeyY) - break - case KeyFUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyY) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyG: - err = keyboard.KeyPress(uinput.KeyU) - break - case KeyGUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyU) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyH: - err = keyboard.KeyPress(uinput.KeyJ) - break - case KeyHUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyJ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyI: - err = keyboard.KeyPress(uinput.KeyG) - break - case KeyIUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyG) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyJ: - err = keyboard.KeyPress(uinput.KeyC) - break - case KeyJUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyC) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyK: - err = keyboard.KeyPress(uinput.KeyV) - break - case KeyKUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyV) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyL: - err = keyboard.KeyPress(uinput.KeyP) - break - case KeyLUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyP) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyM: - err = keyboard.KeyPress(uinput.KeyM) - break - case KeyMUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyM) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyN: - err = keyboard.KeyPress(uinput.KeyL) - break - case KeyNUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyL) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyO: - err = keyboard.KeyPress(uinput.KeyS) - break - case KeyOUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyS) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyP: - err = keyboard.KeyPress(uinput.KeyR) - break - case KeyPUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyR) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyQ: - err = keyboard.KeyPress(uinput.KeyX) - break - case KeyQUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyX) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyR: - err = keyboard.KeyPress(uinput.KeyO) - break - case KeyRUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyO) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyS: - err = keyboard.KeyPress(uinput.KeySemicolon) - break - case KeySUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySemicolon) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyT: - err = keyboard.KeyPress(uinput.KeyK) - break - case KeyTUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyK) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyU: - err = keyboard.KeyPress(uinput.KeyF) - break - case KeyUUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyF) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyV: - err = keyboard.KeyPress(uinput.KeyDot) - break - case KeyVUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyDot) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyW: - err = keyboard.KeyPress(uinput.KeyComma) - break - case KeyWUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyComma) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyX: - err = keyboard.KeyPress(uinput.KeyB) - break - case KeyXUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyB) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyY: - err = keyboard.KeyPress(uinput.KeyT) - break - case KeyYUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyT) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyZ: - err = keyboard.KeyPress(uinput.KeySlash) - break - case KeyZUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySlash) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case Key1: - err = keyboard.KeyPress(uinput.Key1) - break - case Key2: - err = keyboard.KeyPress(uinput.Key2) - break - case Key3: - err = keyboard.KeyPress(uinput.Key3) - break - case Key4: - err = keyboard.KeyPress(uinput.Key4) - break - case Key5: - err = keyboard.KeyPress(uinput.Key5) - break - case Key6: - err = keyboard.KeyPress(uinput.Key6) - break - case Key7: - err = keyboard.KeyPress(uinput.Key7) - break - case Key8: - err = keyboard.KeyPress(uinput.Key8) - break - case Key9: - err = keyboard.KeyPress(uinput.Key9) - break - case Key0: - err = keyboard.KeyPress(uinput.Key0) - break - case KeyTab: - err = keyboard.KeyPress(uinput.KeyTab) - break - case KeyHyphen: - err = keyboard.KeyPress(uinput.KeyApostrophe) - break - case KeyExclamationMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key1) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAtSign: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key2) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyHash: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key3) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDollar: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key4) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyPercent: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key5) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyCaret: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key6) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAmpersand: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key7) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAsterisk: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key8) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDot: - err = keyboard.KeyPress(uinput.KeyE) - break - case KeyComma: - err = keyboard.KeyPress(uinput.KeyW) - break - case KeyQuestionMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyLeftbrace) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySemicolon: - err = keyboard.KeyPress(uinput.KeyZ) - break - case KeyColon: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyZ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySlash: - err = keyboard.KeyPress(uinput.KeyLeftbrace) - break - case KeyApostrophe: - err = keyboard.KeyPress(uinput.KeyQ) - break - case KeySpace: - err = keyboard.KeyPress(uinput.KeySpace) - break - - default: - fmt.Println("Unknown key: ", key) - fmt.Println("Please add it to the dvorak layout") - return errors.New("Unknown key") - } - - return err -} - -func init() { - DefaultLayoutRegistry.Register("dvorak", Dvorak{}) -} diff --git a/autofill/autotype/uinput/qwerty.go b/autofill/autotype/uinput/qwerty.go deleted file mode 100644 index 79cc88b..0000000 --- a/autofill/autotype/uinput/qwerty.go +++ /dev/null @@ -1,387 +0,0 @@ -package uinput - -import ( - "errors" - "fmt" - - "github.com/bendahl/uinput" -) - -type Qwerty struct { -} - -func (d Qwerty) TypeKey(key Key, keyboard uinput.Keyboard) error { - var err error - - switch key { - case KeyA: - err = keyboard.KeyPress(uinput.KeyA) - break - case KeyAUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyA) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyB: - err = keyboard.KeyPress(uinput.KeyB) - break - case KeyBUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyB) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyC: - err = keyboard.KeyPress(uinput.KeyC) - break - case KeyCUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyC) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyD: - err = keyboard.KeyPress(uinput.KeyD) - break - case KeyDUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyD) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyE: - err = keyboard.KeyPress(uinput.KeyE) - break - case KeyEUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyE) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyF: - err = keyboard.KeyPress(uinput.KeyF) - break - case KeyFUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyF) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyG: - err = keyboard.KeyPress(uinput.KeyG) - break - case KeyGUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyG) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyH: - err = keyboard.KeyPress(uinput.KeyH) - break - case KeyHUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyH) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyI: - err = keyboard.KeyPress(uinput.KeyI) - break - case KeyIUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyI) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyJ: - err = keyboard.KeyPress(uinput.KeyJ) - break - case KeyJUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyJ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyK: - err = keyboard.KeyPress(uinput.KeyK) - break - case KeyKUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyK) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyL: - err = keyboard.KeyPress(uinput.KeyL) - break - case KeyLUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyL) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyM: - err = keyboard.KeyPress(uinput.KeyM) - break - case KeyMUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyM) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyN: - err = keyboard.KeyPress(uinput.KeyN) - break - case KeyNUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyN) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyO: - err = keyboard.KeyPress(uinput.KeyO) - break - case KeyOUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyO) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyP: - err = keyboard.KeyPress(uinput.KeyP) - break - case KeyPUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyP) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyQ: - err = keyboard.KeyPress(uinput.KeyQ) - break - case KeyQUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyQ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyR: - err = keyboard.KeyPress(uinput.KeyR) - break - case KeyRUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyR) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyS: - err = keyboard.KeyPress(uinput.KeyS) - break - case KeySUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyS) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyT: - err = keyboard.KeyPress(uinput.KeyT) - break - case KeyTUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyT) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyU: - err = keyboard.KeyPress(uinput.KeyU) - break - case KeyUUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyU) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyV: - err = keyboard.KeyPress(uinput.KeyV) - break - case KeyVUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyV) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyW: - err = keyboard.KeyPress(uinput.KeyW) - break - case KeyWUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyW) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyX: - err = keyboard.KeyPress(uinput.KeyX) - break - case KeyXUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyX) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyY: - err = keyboard.KeyPress(uinput.KeyY) - break - case KeyYUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyY) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case KeyZ: - err = keyboard.KeyPress(uinput.KeyZ) - break - case KeyZUpper: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeyZ) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - case Key1: - err = keyboard.KeyPress(uinput.Key1) - break - case Key2: - err = keyboard.KeyPress(uinput.Key2) - break - case Key3: - err = keyboard.KeyPress(uinput.Key3) - break - case Key4: - err = keyboard.KeyPress(uinput.Key4) - break - case Key5: - err = keyboard.KeyPress(uinput.Key5) - break - case Key6: - err = keyboard.KeyPress(uinput.Key6) - break - case Key7: - err = keyboard.KeyPress(uinput.Key7) - break - case Key8: - err = keyboard.KeyPress(uinput.Key8) - break - case Key9: - err = keyboard.KeyPress(uinput.Key9) - break - case Key0: - err = keyboard.KeyPress(uinput.Key0) - break - case KeyTab: - err = keyboard.KeyPress(uinput.KeyTab) - break - case KeyHyphen: - err = keyboard.KeyPress(uinput.KeyMinus) - break - case KeyExclamationMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key1) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAtSign: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key2) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyHash: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key3) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDollar: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key4) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyPercent: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key5) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyCaret: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key6) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAmpersand: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key7) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyAsterisk: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.Key8) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeyDot: - err = keyboard.KeyPress(uinput.KeyDot) - break - case KeyComma: - err = keyboard.KeyPress(uinput.KeyW) - break - case KeyQuestionMark: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySlash) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySemicolon: - err = keyboard.KeyPress(uinput.KeySemicolon) - break - case KeyColon: - err = keyboard.KeyDown(uinput.KeyLeftshift) - Sleep() - err = keyboard.KeyPress(uinput.KeySemicolon) - Sleep() - err = keyboard.KeyUp(uinput.KeyLeftshift) - break - case KeySlash: - err = keyboard.KeyPress(uinput.KeySlash) - break - case KeyApostrophe: - err = keyboard.KeyPress(uinput.KeyApostrophe) - break - - case KeySpace: - err = keyboard.KeyPress(uinput.KeySpace) - break - - default: - fmt.Println("Unknown key: ", key) - fmt.Println("Please add it to the QWERTY layout") - return errors.New("Unknown key") - } - return err -} - -func init() { - DefaultLayoutRegistry.Register("qwerty", Qwerty{}) -} diff --git a/autofill/autotype/uinput/uinput.go b/autofill/autotype/uinput/uinput.go deleted file mode 100644 index 8e32fb5..0000000 --- a/autofill/autotype/uinput/uinput.go +++ /dev/null @@ -1,175 +0,0 @@ -package uinput - -import ( - "errors" - "fmt" - "time" - - "github.com/bendahl/uinput" -) - -type Layout interface { - TypeKey(key Key, keyboard uinput.Keyboard) error -} - -type Key string - -const ( - KeyA Key = "a" - KeyB Key = "b" - KeyC Key = "c" - KeyD Key = "d" - KeyE Key = "e" - KeyF Key = "f" - KeyG Key = "g" - KeyH Key = "h" - KeyI Key = "i" - KeyJ Key = "j" - KeyK Key = "k" - KeyL Key = "l" - KeyM Key = "m" - KeyN Key = "n" - KeyO Key = "o" - KeyP Key = "p" - KeyQ Key = "q" - KeyR Key = "r" - KeyS Key = "s" - KeyT Key = "t" - KeyU Key = "u" - KeyV Key = "v" - KeyW Key = "w" - KeyX Key = "x" - KeyY Key = "y" - KeyZ Key = "z" - KeyAUpper Key = "A" - KeyBUpper Key = "B" - KeyCUpper Key = "C" - KeyDUpper Key = "D" - KeyEUpper Key = "E" - KeyFUpper Key = "F" - KeyGUpper Key = "G" - KeyHUpper Key = "H" - KeyIUpper Key = "I" - KeyJUpper Key = "J" - KeyKUpper Key = "K" - KeyLUpper Key = "L" - KeyMUpper Key = "M" - KeyNUpper Key = "N" - KeyOUpper Key = "O" - KeyPUpper Key = "P" - KeyQUpper Key = "Q" - KeyRUpper Key = "R" - KeySUpper Key = "S" - KeyTUpper Key = "T" - KeyUUpper Key = "U" - KeyVUpper Key = "V" - KeyWUpper Key = "W" - KeyXUpper Key = "X" - KeyYUpper Key = "Y" - KeyZUpper Key = "Z" - Key0 Key = "0" - Key1 Key = "1" - Key2 Key = "2" - Key3 Key = "3" - Key4 Key = "4" - Key5 Key = "5" - Key6 Key = "6" - Key7 Key = "7" - Key8 Key = "8" - Key9 Key = "9" - - KeyHyphen Key = "-" - - KeySpace Key = " " - KeyExclamationMark Key = "!" - KeyAtSign Key = "@" - KeyHash Key = "#" - KeyDollar Key = "$" - KeyPercent Key = "%" - KeyCaret Key = "^" - KeyAmpersand Key = "&" - KeyAsterisk Key = "*" - - KeyDot Key = "." - KeyComma Key = "," - KeySlash Key = "/" - KeyBackslash Key = "\\" - KeyQuestionMark Key = "?" - KeySemicolon Key = ";" - KeyColon Key = ":" - KeyApostrophe Key = "'" - - KeyTab Key = "\t" -) - -type LayoutRegistry struct { - layouts map[string]Layout -} - -func NewLayoutRegistry() *LayoutRegistry { - return &LayoutRegistry{ - layouts: make(map[string]Layout), - } -} - -var DefaultLayoutRegistry = NewLayoutRegistry() - -func (r *LayoutRegistry) Register(name string, layout Layout) { - r.layouts[name] = layout -} - -func TypeString(text string, layout string) error { - if layout == "" { - layout = "qwerty" - } - - if _, ok := DefaultLayoutRegistry.layouts[layout]; !ok { - return errors.New("layout not found") - } - - keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("testkeyboard")) - if err != nil { - return err - } - - for _, c := range text { - key := Key(string(c)) - err := DefaultLayoutRegistry.layouts[layout].TypeKey(key, keyboard) - if err != nil { - fmt.Println(err) - } - time.Sleep(10 * time.Millisecond) - } - - err = keyboard.Close() - if err != nil { - return err - } - - return nil -} - -func Paste(layout string) error { - if layout == "" { - layout = "qwerty" - } - - if _, ok := DefaultLayoutRegistry.layouts[layout]; !ok { - return errors.New("layout not found") - } - - keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("Goldwarden Autotype")) - if err != nil { - return err - } - keyboard.KeyDown(uinput.KeyLeftctrl) - time.Sleep(100 * time.Millisecond) - DefaultLayoutRegistry.layouts[layout].TypeKey(KeyV, keyboard) - time.Sleep(100 * time.Millisecond) - keyboard.KeyUp(uinput.KeyLeftctrl) - return nil -} - -func Sleep() { - time.Sleep(20 * time.Millisecond) -} diff --git a/autofill/autotype/uinputautotype.go b/autofill/autotype/uinputautotype.go deleted file mode 100644 index 50bb452..0000000 --- a/autofill/autotype/uinputautotype.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build linux && uinput - -package autotype - -import "github.com/quexten/goldwarden/autofill/autotype/uinput" - -func TypeString(text string, layout string) error { - return uinput.TypeString(text, layout) -} diff --git a/autofill/autotype/unimplemented.go b/autofill/autotype/unimplemented.go deleted file mode 100644 index a7d8c73..0000000 --- a/autofill/autotype/unimplemented.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux - -package autotype - -func TypeString(text string, layout string) error { - return errors.New("Not implemented") -} diff --git a/autofill/gioAutofillDialog.go b/autofill/gioAutofillDialog.go deleted file mode 100644 index f3ce170..0000000 --- a/autofill/gioAutofillDialog.go +++ /dev/null @@ -1,336 +0,0 @@ -//go:build !noautofill - -package autofill - -import ( - "fmt" - "image" - "image/color" - "log" - "math" - "os" - "os/user" - "sort" - "strings" - - "gioui.org/app" - "gioui.org/font/gofont" - "gioui.org/io/key" - "gioui.org/io/system" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" - "gioui.org/unit" - "gioui.org/widget" - "gioui.org/widget/material" - "github.com/shirou/gopsutil/v3/process" -) - -type AutofillEntry struct { - Username string - Name string - UUID string -} - -var autofillEntries = []AutofillEntry{} -var onAutofill func(string, chan bool) -var selectedEntry = 0 - -type scoredAutofillEntry struct { - autofillEntry AutofillEntry - score int -} - -func getUserProcs() []string { - procNames := []string{} - - procs, err := process.Processes() - if err != nil { - return []string{} - } - for _, proc := range procs { - user, err := user.Current() - if err != nil { - continue - } - - procuser, err := proc.Username() - if err != nil { - continue - } - if procuser == user.Username { - procName, err := proc.Name() - if err != nil { - continue - } - procNames = append(procNames, strings.ToLower(procName)) - } - - } - - return procNames -} - -func GetFilteredAutofillEntries(entries []AutofillEntry, filter string) []AutofillEntry { - if len(filter) == 0 { - return []AutofillEntry{} - } - - filter = strings.ToLower(filter) - - // filter entrien by whether they contain the filter string - filteredEntries := []AutofillEntry{} - for _, entry := range entries { - name := strings.ToLower(entry.Name) - if strings.HasPrefix(name, filter) { - filteredEntries = append(filteredEntries, entry) - } - } - - processes := getUserProcs() - - scoredEntries := []scoredAutofillEntry{} - for _, entry := range filteredEntries { - score := 0 - name := strings.ToLower(entry.Name) - filter = strings.ToLower(filter) - - maxProcessScore := 0 - - for _, process := range processes { - score := 0 - - sharedPrefixLen := 0 - for i := 0; i < len(process) && i < len(entry.Name); i++ { - if process[i] == name[i] { - sharedPrefixLen++ - } else { - break - } - } - sharedPrefixLenPercent := float32(sharedPrefixLen) / float32(math.Min(float64(len(process)), float64(len(name)))) - - if sharedPrefixLen > 0 { - score += int(sharedPrefixLenPercent * 7) - } - - if score > maxProcessScore { - maxProcessScore = score - } - } - - score += maxProcessScore - scoredEntries = append(scoredEntries, scoredAutofillEntry{entry, score}) - } - - sort.Slice(scoredEntries, func(i, j int) bool { - return scoredEntries[i].score > scoredEntries[j].score - }) - - var filteredEntries1 []AutofillEntry - for _, scoredEntry := range scoredEntries { - if scoredEntry.score == 0 { - continue - } - filteredEntries1 = append(filteredEntries1, scoredEntry.autofillEntry) - } - - return filteredEntries1 -} - -func RunAutofill(entries []AutofillEntry, onAutofillFunc func(string, chan bool)) { - autofillEntries = entries - onAutofill = onAutofillFunc - - go func() { - w := app.NewWindow() - w.Option(app.Size(unit.Dp(600), unit.Dp(800))) - w.Option(app.Decorated(false)) - w.Perform(system.ActionCenter) - w.Perform(system.ActionRaise) - lineEditor.Focus() - if err := loop(w); err != nil { - log.Fatal(err) - } - }() - app.Main() -} - -var lineEditor = &widget.Editor{ - SingleLine: true, - Submit: true, -} - -var ( - unselected = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} - unselectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} - background = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} - selected = color.NRGBA{R: 0x65, G: 0x1F, B: 0xFF, A: 0xFF} - selectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} -) - -var th = material.NewTheme(gofont.Collection()) -var list = layout.List{Axis: layout.Vertical} - -func doLayout(gtx layout.Context) layout.Dimensions { - var filteredEntries []AutofillEntry = GetFilteredAutofillEntries(autofillEntries, lineEditor.Text()) - - if selectedEntry >= 10 || selectedEntry >= len(filteredEntries) { - selectedEntry = 0 - } - - return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - searchBox := material.Editor(th, lineEditor, "Search query") - searchBox.Color = selectedText - border := widget.Border{Color: selectedText, CornerRadius: unit.Dp(8), Width: unit.Dp(2)} - return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(8)).Layout(gtx, searchBox.Layout) - }) - }) - }) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - - return list.Layout(gtx, len(filteredEntries), func(gtx layout.Context, i int) layout.Dimensions { - entry := filteredEntries[i] - - return layout.Inset{Bottom: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - isSelected := i == selectedEntry - var color color.NRGBA - if isSelected { - color = selected - } else { - color = unselected - } - - return Background{Color: color, CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - dimens := layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - t := material.H6(th, entry.Name) - if isSelected { - t.Color = selectedText - } else { - t.Color = unselectedText - } - return t.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - t := material.Body1(th, entry.Username) - if isSelected { - t.Color = selectedText - } else { - t.Color = unselectedText - } - - return t.Layout(gtx) - }), - ) - return dimens - }) - }) - }) - }) - }) - })) - }) -} - -func loop(w *app.Window) error { - var ops op.Ops - for { - e := <-w.Events() - switch e := e.(type) { - case system.DestroyEvent: - return e.Err - case system.FrameEvent: - gtx := layout.NewContext(&ops, e) - - key.InputOp{ - Keys: key.Set(key.NameReturn + "|" + key.NameEscape + "|" + key.NameDownArrow), - Tag: 0, - }.Add(gtx.Ops) - t := lineEditor.Events() - for _, ev := range t { - switch ev.(type) { - case widget.SubmitEvent: - entries := GetFilteredAutofillEntries(autofillEntries, lineEditor.Text()) - if len(entries) == 0 { - fmt.Println("no entries") - continue - } else { - w.Perform(system.ActionMinimize) - c := make(chan bool) - go onAutofill(entries[selectedEntry].UUID, c) - go func() { - <-c - os.Exit(0) - }() - } - } - } - - test := gtx.Events(0) - for _, ev := range test { - switch ev := ev.(type) { - case key.Event: - switch ev.Name { - case key.NameReturn: - fmt.Println("uncaught submit") - return nil - case key.NameDownArrow: - if ev.State == key.Press { - selectedEntry++ - if selectedEntry >= 10 { - selectedEntry = 0 - } - } - case key.NameEscape: - os.Exit(0) - } - } - } - - doLayout(gtx) - e.Frame(gtx.Ops) - } - } -} - -type Background struct { - Color color.NRGBA - CornerRadius unit.Dp -} - -func (b Background) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - m := op.Record(gtx.Ops) - dims := w(gtx) - size := dims.Size - call := m.Stop() - if r := gtx.Dp(b.CornerRadius); r > 0 { - defer clip.RRect{ - Rect: image.Rect(0, 0, size.X, size.Y), - NE: r, NW: r, SE: r, SW: r, - }.Push(gtx.Ops).Pop() - } - fill{b.Color}.Layout(gtx, size) - call.Add(gtx.Ops) - return dims -} - -type fill struct { - col color.NRGBA -} - -func (f fill) Layout(gtx layout.Context, sz image.Point) layout.Dimensions { - defer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() - paint.ColorOp{Color: f.col}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{Size: sz} -} diff --git a/autofill/autotype/libportalautotype.go b/autotype/libportalautotype.go similarity index 96% rename from autofill/autotype/libportalautotype.go rename to autotype/libportalautotype.go index 64ad3fb..fbd09b2 100644 --- a/autofill/autotype/libportalautotype.go +++ b/autotype/libportalautotype.go @@ -1,4 +1,4 @@ -//go:build linux && !uinput +//go:build linux package autotype @@ -13,7 +13,7 @@ var globalID = 0 const autoTypeDelay = 1 * time.Millisecond -func TypeString(textToType string, layout string) { +func TypeString(textToType string) { bus, err := dbus.SessionBus() if err != nil { panic(err) diff --git a/cmd/autofill.go b/cmd/autofill.go index 1710720..e14a1b3 100644 --- a/cmd/autofill.go +++ b/cmd/autofill.go @@ -1,24 +1,25 @@ -//go:build !noautofill +//go:build linux package cmd import ( - "github.com/quexten/goldwarden/autofill" + "github.com/quexten/goldwarden/autotype" "github.com/spf13/cobra" ) var autofillCmd = &cobra.Command{ - Use: "autofill", - Short: "Autofill credentials", - Long: `Autofill credentials`, + Use: "autotype", + Short: "Autotype credentials", + Long: `Autotype credentials`, Run: func(cmd *cobra.Command, args []string) { - layout := cmd.Flag("layout").Value.String() - autofill.Run(layout, commandClient) + username, _ := cmd.Flags().GetString("username") + password, _ := cmd.Flags().GetString("password") + autotype.TypeString(username + "\t" + password) }, } func init() { rootCmd.AddCommand(autofillCmd) - autofillCmd.PersistentFlags().String("layout", "qwerty", "") - autofillCmd.PersistentFlags().Bool("use-copy-paste", false, "") + autofillCmd.PersistentFlags().String("username", "", "") + autofillCmd.PersistentFlags().String("password", "", "") } diff --git a/cmd/logins.go b/cmd/logins.go index 7d4d530..4251fac 100644 --- a/cmd/logins.go +++ b/cmd/logins.go @@ -1,8 +1,13 @@ package cmd import ( + "encoding/json" + "errors" "fmt" + "strings" + "github.com/icza/gox/stringsx" + "github.com/quexten/goldwarden/client" "github.com/quexten/goldwarden/ipc/messages" "github.com/spf13/cobra" ) @@ -54,6 +59,61 @@ var getLoginCmd = &cobra.Command{ }, } +var listLoginsCmd = &cobra.Command{ + Use: "list", + Short: "Lists all logins in your vault", + Long: `Lists all logins in your vault.`, + Run: func(cmd *cobra.Command, args []string) { + loginIfRequired() + + logins, err := ListLogins(commandClient) + if err != nil { + handleSendToAgentError(err) + return + } + + fmt.Println("[") + for index, login := range logins { + data := map[string]string{ + "name": stringsx.Clean(login.Name), + "uuid": stringsx.Clean(login.UUID), + "username": stringsx.Clean(login.Username), + "password": stringsx.Clean(strings.ReplaceAll(login.Password, "\"", "\\\"")), + } + jsonString, err := json.Marshal(data) + if err != nil { + handleSendToAgentError(err) + return + } + fmt.Print(string(jsonString)) + if index != len(logins)-1 { + fmt.Println(",") + } else { + fmt.Println() + } + } + fmt.Println("]") + }, +} + +func ListLogins(client client.Client) ([]messages.DecryptedLoginCipher, error) { + resp, err := client.SendToAgent(messages.ListLoginsRequest{}) + if err != nil { + return []messages.DecryptedLoginCipher{}, err + } + + switch resp.(type) { + case messages.GetLoginsResponse: + castedResponse := (resp.(messages.GetLoginsResponse)) + return castedResponse.Result, nil + case messages.ActionResponse: + castedResponse := (resp.(messages.ActionResponse)) + return []messages.DecryptedLoginCipher{}, errors.New("Error: " + castedResponse.Message) + default: + return []messages.DecryptedLoginCipher{}, errors.New("Wrong response type") + } +} + func init() { rootCmd.AddCommand(baseLoginCmd) baseLoginCmd.AddCommand(getLoginCmd) @@ -61,4 +121,5 @@ func init() { getLoginCmd.PersistentFlags().String("username", "", "") getLoginCmd.PersistentFlags().String("uuid", "", "") getLoginCmd.PersistentFlags().Bool("full", false, "") + baseLoginCmd.AddCommand(listLoginsCmd) } diff --git a/cmd/vault.go b/cmd/vault.go index 397a13c..2668ede 100644 --- a/cmd/vault.go +++ b/cmd/vault.go @@ -107,9 +107,11 @@ var statusCmd = &cobra.Command{ switch result.(type) { case messages.VaultStatusResponse: status := result.(messages.VaultStatusResponse) - fmt.Println("Locked: ", status.Locked) - fmt.Println("Number of logins: ", status.NumberOfLogins) - fmt.Println("Number of notes: ", status.NumberOfNotes) + fmt.Println("{") + fmt.Println(" \"locked\":", status.Locked, ",") + fmt.Println(" \"loginEntries\":", status.NumberOfLogins, ",") + fmt.Println(" \"noteEntries\":", status.NumberOfNotes) + fmt.Println("}") default: println("Wrong response type") } diff --git a/go.mod b/go.mod index 59ce90b..a1d92ce 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,9 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 // indirect + github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5 // indirect + github.com/k0kubun/pp v2.4.0+incompatible // indirect + github.com/k0kubun/pp/v3 v3.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/go.sum b/go.sum index 3ebe52e..6046d5d 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,14 @@ github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5 h1:K7KEFpKgVcjj98jOu2Z3xMBTtTwfYVT90Zmo3ZuWmbE= +github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/k0kubun/pp v2.4.0+incompatible h1:M9iQzcejGfiBjDa7+Tc0rJgR7WFKP6rim/Q0DDrAT3g= +github.com/k0kubun/pp v2.4.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= +github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= github.com/keys-pub/go-libfido2 v1.5.3 h1:vtgHxlSB43u6lj0TSuA3VvT6z3E7VI+L1a2hvMFdECk= github.com/keys-pub/go-libfido2 v1.5.3/go.mod h1:P0V19qHwJNY0htZwZDe9Ilvs/nokGhdFX7faKFyZ6+U= github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 h1:m81erW+1MD5vl3lKQ/+TYPHJ6Y9/C1COqxXPE51FkDk= diff --git a/ui/autotype/autotype.py b/ui/autotype/autotype.py new file mode 100644 index 0000000..2f75c12 --- /dev/null +++ b/ui/autotype/autotype.py @@ -0,0 +1,101 @@ +# TODO??!?!? for now using golang implementation + +import dbus +import dbus.mainloop.glib +from dbus.mainloop.glib import DBusGMainLoop + +from gi.repository import GLib + +import random +import time + +step = 0 + +def typestring(text): + step = 0 + handle = "" + + def handler(*args, **kwargs): + global step + if step == 0: + handle_xdp_session_created(*args, **kwargs) + elif step == 1: + handle_xdp_devices_selected(*args, **kwargs) + elif step == 2: + handle_session_start(*args, **kwargs) + else: + print(args, kwargs) + step += 1 + + def handle_session_start(code, results, object_path): + global handle + + if code != 0: + raise Exception("Could not start session") + + for sym in text: + if sym == "\t": + inter.NotifyKeyboardKeycode(handle, {}, 15, 1) + time.sleep(0.001) + inter.NotifyKeyboardKeycode(handle, {}, 15, 0) + time.sleep(0.001) + else: + inter.NotifyKeyboardKeysym(handle, {}, ord(sym), 1) + time.sleep(0.001) + inter.NotifyKeyboardKeysym(handle, {}, ord(sym), 0) + time.sleep(0.001) + + bus + + def handle_xdp_devices_selected(code, results, object_path): + global handle + + if code != 0: + raise Exception("Could not select devices") + + start_options = { + "handle_token": "krfb" + str(random.randint(0, 999999999)) + } + reply = inter.Start(handle, "", start_options) + print(reply) + + def handle_xdp_session_created(code, results, object_path): + global handle + + if code != 0: + raise Exception("Could not create session") + print(results) + handle = results["session_handle"] + + # select sources for the session + selection_options = { + "types": dbus.UInt32(7), # request all (KeyBoard, Pointer, TouchScreen) + "handle_token": "krfb" + str(random.randint(0, 999999999)) + } + selector_reply = inter.SelectDevices(handle, selection_options) + print(selector_reply) + + def main(): + global bus + global inter + loop = GLib.MainLoop() + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + bus = dbus.SessionBus() + obj = bus.get_object("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop") + inter = dbus.Interface(obj, "org.freedesktop.portal.RemoteDesktop") + + bus.add_signal_receiver( + handler, + signal_name="Response", + dbus_interface="org.freedesktop.portal.Request", + bus_name="org.freedesktop.portal.Desktop", + path_keyword="object_path") + + print(inter) + result = inter.CreateSession({ + "session_handle_token": "sessionhandletoken" + }) + print(result) + loop.run() + + main() diff --git a/ui/clipboard.py b/ui/clipboard.py new file mode 100644 index 0000000..818da99 --- /dev/null +++ b/ui/clipboard.py @@ -0,0 +1,5 @@ +import subprocess + +def write(text): + process = subprocess.Popen(["/bin/sh", "-c", "wl-copy"], stdin=subprocess.PIPE) + process.communicate(text.encode('utf-8')) \ No newline at end of file diff --git a/ui/goldwarden.py b/ui/goldwarden.py new file mode 100644 index 0000000..6e4e90a --- /dev/null +++ b/ui/goldwarden.py @@ -0,0 +1,87 @@ +import subprocess +import json + +BINARY_PATH = "/home/quexten/go/src/github.com/quexten/goldwarden/goldwarden" + +def set_api_url(url): + restic_cmd = f"{BINARY_PATH} config set-api-url {url}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def set_identity_url(url): + restic_cmd = f"{BINARY_PATH} config set-identity-url {url}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def set_notification_url(url): + restic_cmd = f"{BINARY_PATH} config set-notifications-url {url}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def login_with_password(email, password): + restic_cmd = f"{BINARY_PATH} vault login --email {email}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + if len(result.stderr.strip()) > 0: + print(result.stderr) + if "password" in result.stderr: + return "badpass" + else: + if "Logged in" in result.stderr: + print("ok") + return "ok" + return "error" + print("ok") + return "ok" + +def login_passwordless(email): + restic_cmd = f"{BINARY_PATH} vault login --email {email} --passwordless" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def is_pin_enabled(): + restic_cmd = f"{BINARY_PATH} vault pin" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + # check if contains enabled + return "enabled" in result.stdout + +def enable_pin(): + restic_cmd = f"{BINARY_PATH} vault pin set" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + +def get_vault_status(): + restic_cmd = f"{BINARY_PATH} vault status" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + try: + return json.loads(result.stdout) + except Exception as e: + print(e) + return None + +def get_vault_logins(): + restic_cmd = f"{BINARY_PATH} logins list" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) + try: + return json.loads(result.stdout) + except Exception as e: + print(e) + return None + +def autotype(username, password): + restic_cmd = f"{BINARY_PATH} autotype --username {username} --password {password}" + result = subprocess.run(restic_cmd.split(), capture_output=True, text=True) + if result.returncode != 0: + raise Exception("Failed to initialize repository, err", result.stderr) \ No newline at end of file diff --git a/ui/main.py b/ui/main.py new file mode 100644 index 0000000..293b4c4 --- /dev/null +++ b/ui/main.py @@ -0,0 +1,286 @@ +#!/usr/bin/python +import sys +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +import gc + +from gi.repository import Gtk, Adw, Gdk, Graphene, Gsk, Gio, GLib, GObject +import monitors.dbus_autofill_monitor +import goldwarden +import clipboard +import time +from threading import Thread + +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") + # on type func + def on_type(entry): + if len(entry.get_text()) > 1: + self.history_list.show() + else: + self.history_list.hide() + + while self.history_list.get_first_child() != None: + self.history_list.remove(self.history_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"] + self.history_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.history_list = Gtk.ListBox() + # margin' + self.history_list.set_margin_start(10) + self.history_list.set_margin_end(10) + self.history_list.set_margin_top(10) + self.history_list.set_margin_bottom(10) + self.history_list.hide() + + keycont = Gtk.EventControllerKey() + def handle_keypress(controller, keyval, keycode, state, user_data): + if keycode == 36: + print("enter") + self.hide() + def do_autotype(username, password): + time.sleep(0.5) + goldwarden.autotype(username, password) + GLib.idle_add(lambda: self.show()) + autotypeThread = Thread(target=do_autotype, args=(self.history_list.get_selected_row().username, self.history_list.get_selected_row().password,)) + autotypeThread.start() + print(self.history_list.get_selected_row().get_title()) + if keyval == 112: + print("copy password") + clipboard.write(self.history_list.get_selected_row().password) + elif keyval == 117: + print("copy username") + clipboard.write(self.history_list.get_selected_row().username) + + keycont.connect('key-pressed', handle_keypress, self) + self.add_controller(keycont) + + self.history_list.get_style_context().add_class("boxed-list") + self.box.append(self.history_list) + self.set_default_size(700, 700) + self.set_title("Goldwarden") + + def on_close(window): + while self.history_list.get_first_child() != None: + self.history_list.remove(self.history_list.get_first_child()) + window.hide() + gc.collect() + return True + self.connect("close-request", on_close) + + def show(self): + for i in range(0, 5): + action_row = Adw.ActionRow() + action_row.set_title("aaa") + action_row.set_subtitle("Test") + self.history_list.append(action_row) + + +class SettingsWinvdow(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.preferences_page = Adw.PreferencesPage() + self.preferences_page.set_title("General") + self.stack.add_named(self.preferences_page, "preferences_page") + + self.preferences_group = Adw.PreferencesGroup() + self.preferences_group.set_title("Services") + self.preferences_page.add(self.preferences_group) + + self.ssh_row = Adw.ActionRow() + self.ssh_row.set_title("SSH Daemon") + self.ssh_row.set_subtitle("Listening at ~/.goldwarden-ssh-agent.sock") + self.preferences_group.add(self.ssh_row) + + self.login_with_device = Adw.ActionRow() + self.login_with_device.set_title("Login with device") + self.login_with_device.set_subtitle("Waiting for requests...") + self.preferences_group.add(self.login_with_device) + + self.autofill_row = Adw.ActionRow() + self.autofill_row.set_title("Autofill Shortcut") + self.autofill_row.set_subtitle("Unavailable") + self.preferences_group.add(self.autofill_row) + + self.status_row = Adw.ActionRow() + self.status_row.set_title("DBUS Service") + self.status_row.set_subtitle("Listening") + self.preferences_group.add(self.status_row) + + self.status_row = Adw.ActionRow() + self.status_row.set_title("Vault Status") + self.status_row.set_subtitle("Locked") + self.preferences_group.add(self.status_row) + + self.login_row = Adw.ActionRow() + self.login_row.set_title("Vault Login Entries") + self.login_row.set_subtitle("0") + self.preferences_group.add(self.login_row) + + self.notes_row = Adw.ActionRow() + self.notes_row.set_title("Vault Notes") + self.notes_row.set_subtitle("0") + self.preferences_group.add(self.notes_row) + + self.login_button = Gtk.Button() + self.login_button.set_label("Login") + self.login_button.connect("clicked", lambda button: show_login()) + self.login_button.set_margin_top(10) + self.preferences_group.add(self.login_button) + + self.set_pin_button = Gtk.Button() + self.set_pin_button.set_label("Set Pin") + def set_pin(): + set_pin_thread = Thread(target=goldwarden.enable_pin) + set_pin_thread.start() + self.set_pin_button.connect("clicked", lambda button: set_pin()) + self.set_pin_button.set_margin_top(10) + self.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) + self.preferences_group.add(self.unlock_button) + + def update_labels(): + status = goldwarden.get_vault_status() + self.status_row.set_subtitle(str("Unlocked" if status["locked"] == False else "Locked")) + self.login_row.set_subtitle(str(status["loginEntries"])) + self.notes_row.set_subtitle(str(status["noteEntries"])) + GLib.timeout_add(1000, update_labels) + + GLib.timeout_add(1000, update_labels) + self.set_default_size(400, 700) + self.set_title("Goldwarden") + + + #add title buttons + self.title_bar = Gtk.HeaderBar() + self.set_titlebar(self.title_bar) + +class MyApp(Adw.Application): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.connect('activate', self.on_activate) + + def on_activate(self, app): + if hasattr(self, "win") is False: + app.hold() + self.win = MainWindow(application=app) + self.win.set_hide_on_close(True) + self.win.hide() + self.settings_win = SettingsWinvdow(application=app) + self.settings_win.set_hide_on_close(True) + self.settings_win.present() + +app = MyApp(application_id="com.quexten.Goldwarden") +def on_autofill(): + logins = goldwarden.get_vault_logins() + if logins == None: + return + app.win.logins = logins + app.win.show() + app.win.present() +monitors.dbus_autofill_monitor.on_autofill = on_autofill + +def show_login(): + dialog = Gtk.Dialog(title="Goldwarden") + preference_group = Adw.PreferencesGroup() + preference_group.set_title("Config") + dialog.get_content_area().append(preference_group) + + api_url_entry = Adw.EntryRow() + api_url_entry.set_title("API Url") + # set value + api_url_entry.set_text("https://api.bitwarden.com/") + preference_group.add(api_url_entry) + + identity_url_entry = Adw.EntryRow() + identity_url_entry.set_title("Identity Url") + identity_url_entry.set_text("https://identity.bitwarden.com/") + preference_group.add(identity_url_entry) + + notification_url_entry = Adw.EntryRow() + notification_url_entry.set_title("Notification URL") + notification_url_entry.set_text("https://notifications.bitwarden.com/") + preference_group.add(notification_url_entry) + + auth_preference_group = Adw.PreferencesGroup() + auth_preference_group.set_title("Authentication") + dialog.get_content_area().append(auth_preference_group) + + email_entry = Adw.EntryRow() + email_entry.set_title("Email") + email_entry.set_text("") + auth_preference_group.add(email_entry) + + dialog.add_button("Login", Gtk.ResponseType.OK) + def on_save(res): + if res != Gtk.ResponseType.OK: + return + goldwarden.set_api_url(api_url_entry.get_text()) + goldwarden.set_identity_url(identity_url_entry.get_text()) + goldwarden.set_notification_url(notification_url_entry.get_text()) + def login(): + res = goldwarden.login_with_password(email_entry.get_text(), "password") + def handle_res(): + print("handle res", res) + if res == "ok": + dialog.close() + print("ok") + 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() + + #ok response + dialog.connect("response", lambda dialog, response: on_save(response)) + dialog.set_default_size(400, 200) + dialog.set_modal(True) + dialog.present() + + +app.run(sys.argv) + diff --git a/ui/monitors/dbus_autofill_monitor.py b/ui/monitors/dbus_autofill_monitor.py new file mode 100644 index 0000000..b32788f --- /dev/null +++ b/ui/monitors/dbus_autofill_monitor.py @@ -0,0 +1,23 @@ +#Python DBUS Test Server +#runs until the Quit() method is called via DBUS + +from gi.repository import Gtk +import dbus +import dbus.service +from dbus.mainloop.glib import DBusGMainLoop +from threading import Thread + +on_autofill = lambda: None + +class GoldwardenDBUSService(dbus.service.Object): + def __init__(self): + bus_name = dbus.service.BusName('com.quexten.goldwarden', bus=dbus.SessionBus()) + dbus.service.Object.__init__(self, bus_name, '/com/quexten/goldwarden') + + @dbus.service.method('com.quexten.goldwarden.Autofill') + def autofill(self): + on_autofill() + return "" + +DBusGMainLoop(set_as_default=True) +service = GoldwardenDBUSService() diff --git a/ui/monitors/global_shortcut_portal.py b/ui/monitors/global_shortcut_portal.py new file mode 100644 index 0000000..99f1089 --- /dev/null +++ b/ui/monitors/global_shortcut_portal.py @@ -0,0 +1 @@ +# todo \ No newline at end of file