commit
92fac7865a
13
README.md
13
README.md
@ -1,8 +1,8 @@
|
|||||||
A search provider for GNOME Shell that adds support for searching in zx2c4/[pass](https://www.passwordstore.org/).
|
A search provider for GNOME Shell that adds support for searching passwords in zx2c4/[pass](https://www.passwordstore.org/) or in the [rbw](https://github.com/doy/rbw) Bitwarden/Vaultwarden client.
|
||||||
|
|
||||||
Names of passwords will show up in GNOME Shell searches, choosing one will copy the corresponding content to the clipboard.
|
Names of passwords will show up in GNOME Shell searches, choosing one will copy the corresponding content to the clipboard.
|
||||||
|
|
||||||
Supports OTP, fields and can use GPaste.
|
Can use the [GPaste](https://github.com/Keruspe/GPaste) clipboard manager, supports OTP and fields (pass only, requires GPaste).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -29,11 +29,12 @@ dnf install gnome-pass-search-provider
|
|||||||
|
|
||||||
## Manual
|
## Manual
|
||||||
|
|
||||||
Ensure that python>=3.5 as well as the dbus, gobject, fuzzywuzzy Python modules are installed. They should all be packaged under python-name or python3-name depending on your distribution.
|
Ensure that python>=3.7 as well as the dbus, gobject and fuzzywuzzy Python modules are installed. They should all be packaged under python-name or python3-name depending on your distribution.
|
||||||
|
|
||||||
Clone this repository and run the installation script as root:
|
Clone this repository and run the installation script as root:
|
||||||
```
|
```
|
||||||
git clone git@github.com:jle64/gnome-shell-pass-search-provider.git
|
git clone https://github.com/jle64/gnome-pass-search-provider.git
|
||||||
|
cd gnome-pass-search-provider
|
||||||
sudo ./install.sh
|
sudo ./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -62,6 +63,10 @@ To copy the pin start the search with `:pin` and for the username with `:user`.
|
|||||||
|
|
||||||
The [pass-otp](https://github.com/tadfisher/pass-otp) extension is supported. Searches starting with `otp` will copy the otp token to the clipboard.
|
The [pass-otp](https://github.com/tadfisher/pass-otp) extension is supported. Searches starting with `otp` will copy the otp token to the clipboard.
|
||||||
|
|
||||||
|
# Bitwarden/Vaultwarden
|
||||||
|
|
||||||
|
If [rbw](https://github.com/doy/rbw) is installed, it can be used instead of pass by prefixing a search with `bw`. Non prefixed searches will still go through pass if present.
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
|
|
||||||
If you are configuring `pass` through environment variables, such as `PASSWORD_STORE_DIR`, make sure to set them in a way that will propagate to the search provider executable, not just in your shell.
|
If you are configuring `pass` through environment variables, such as `PASSWORD_STORE_DIR`, make sure to set them in a way that will propagate to the search provider executable, not just in your shell.
|
||||||
|
@ -51,6 +51,7 @@ class SearchPassService(dbus.service.Object):
|
|||||||
|
|
||||||
bus_name = "org.gnome.Pass.SearchProvider"
|
bus_name = "org.gnome.Pass.SearchProvider"
|
||||||
_object_path = "/" + bus_name.replace(".", "/")
|
_object_path = "/" + bus_name.replace(".", "/")
|
||||||
|
use_bw = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.session_bus = dbus.SessionBus()
|
self.session_bus = dbus.SessionBus()
|
||||||
@ -66,7 +67,14 @@ class SearchPassService(dbus.service.Object):
|
|||||||
|
|
||||||
@dbus.service.method(in_signature="as", out_signature="as", **sbn)
|
@dbus.service.method(in_signature="as", out_signature="as", **sbn)
|
||||||
def GetInitialResultSet(self, terms):
|
def GetInitialResultSet(self, terms):
|
||||||
return self.get_result_set(terms)
|
try:
|
||||||
|
if terms[0] == "bw":
|
||||||
|
self.use_bw = True
|
||||||
|
return self.get_bw_result_set(terms)
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
self.use_bw = False
|
||||||
|
return self.get_pass_result_set(terms)
|
||||||
|
|
||||||
@dbus.service.method(in_signature="as", out_signature="aa{sv}", **sbn)
|
@dbus.service.method(in_signature="as", out_signature="aa{sv}", **sbn)
|
||||||
def GetResultMetas(self, ids):
|
def GetResultMetas(self, ids):
|
||||||
@ -81,13 +89,31 @@ class SearchPassService(dbus.service.Object):
|
|||||||
|
|
||||||
@dbus.service.method(in_signature="asas", out_signature="as", **sbn)
|
@dbus.service.method(in_signature="asas", out_signature="as", **sbn)
|
||||||
def GetSubsearchResultSet(self, previous_results, new_terms):
|
def GetSubsearchResultSet(self, previous_results, new_terms):
|
||||||
return self.get_result_set(new_terms)
|
if self.use_bw:
|
||||||
|
return self.get_bw_result_set(new_terms)
|
||||||
|
else:
|
||||||
|
return self.get_pass_result_set(new_terms)
|
||||||
|
|
||||||
@dbus.service.method(in_signature="asu", terms="as", timestamp="u", **sbn)
|
@dbus.service.method(in_signature="asu", terms="as", timestamp="u", **sbn)
|
||||||
def LaunchSearch(self, terms, timestamp):
|
def LaunchSearch(self, terms, timestamp):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_result_set(self, terms):
|
def get_bw_result_set(self, terms):
|
||||||
|
name = "".join(terms[1:])
|
||||||
|
|
||||||
|
password_list = subprocess.check_output(
|
||||||
|
["rbw", "list"], stderr=subprocess.STDOUT, universal_newlines=True
|
||||||
|
).split("\n")[:-1]
|
||||||
|
|
||||||
|
results = [
|
||||||
|
e[0]
|
||||||
|
for e in process.extract(
|
||||||
|
name, password_list, limit=5, scorer=fuzz.partial_ratio
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_pass_result_set(self, terms):
|
||||||
if terms[0] == "otp":
|
if terms[0] == "otp":
|
||||||
field = terms[0]
|
field = terms[0]
|
||||||
elif terms[0].startswith(":"):
|
elif terms[0].startswith(":"):
|
||||||
@ -123,31 +149,56 @@ class SearchPassService(dbus.service.Object):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def send_password_to_gpaste(self, base_args, name, field=None):
|
def send_password_to_gpaste(self, base_args, name, field=None):
|
||||||
try:
|
gpaste = self.session_bus.get_object(
|
||||||
gpaste = self.session_bus.get_object(
|
"org.gnome.GPaste.Daemon", "/org/gnome/GPaste"
|
||||||
"org.gnome.GPaste.Daemon", "/org/gnome/GPaste"
|
)
|
||||||
)
|
|
||||||
|
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
base_args + [name], stderr=subprocess.STDOUT, universal_newlines=True
|
base_args + [name], stderr=subprocess.STDOUT, universal_newlines=True
|
||||||
|
)
|
||||||
|
if field is not None:
|
||||||
|
match = re.search(
|
||||||
|
fr"^{field}:\s*(?P<value>.+?)$", output, flags=re.I | re.M
|
||||||
)
|
)
|
||||||
if field is not None:
|
if match:
|
||||||
match = re.search(
|
password = match.group("value")
|
||||||
fr"^{field}:\s*(?P<value>.+?)$", output, flags=re.I | re.M
|
|
||||||
)
|
|
||||||
if match:
|
|
||||||
password = match.group("value")
|
|
||||||
else:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"The field {field} was not found in " + "the pass file."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
password = output.split("\n", 1)[0]
|
raise RuntimeError(f"The field {field} was not found in the pass file.")
|
||||||
try:
|
else:
|
||||||
gpaste.AddPassword(name, password, dbus_interface="org.gnome.GPaste1")
|
password = output.split("\n", 1)[0]
|
||||||
except dbus.DBusException:
|
try:
|
||||||
gpaste.AddPassword(name, password, dbus_interface="org.gnome.GPaste2")
|
gpaste.AddPassword(name, password, dbus_interface="org.gnome.GPaste1")
|
||||||
|
except dbus.DBusException:
|
||||||
|
gpaste.AddPassword(name, password, dbus_interface="org.gnome.GPaste2")
|
||||||
|
|
||||||
|
def send_password_to_native_clipboard(self, base_args, name, field=None):
|
||||||
|
if field is not None or self.use_bw:
|
||||||
|
raise RuntimeError("This feature requires GPaste.")
|
||||||
|
|
||||||
|
result = subprocess.run(base_args + ["-c", name])
|
||||||
|
if result.returncode:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Error while running pass: got return code {result.returncode}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_password_to_clipboard(self, name):
|
||||||
|
field = None
|
||||||
|
if self.use_bw:
|
||||||
|
base_args = ["rbw", "get"]
|
||||||
|
elif name.startswith("otp "):
|
||||||
|
base_args = ["pass", "otp", "code"]
|
||||||
|
name = name[4:]
|
||||||
|
else:
|
||||||
|
base_args = ["pass", "show"]
|
||||||
|
if name.startswith(":"):
|
||||||
|
field, name = name.split(" ", 1)
|
||||||
|
field = field[1:]
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self.send_password_to_gpaste(base_args, name, field)
|
||||||
|
except dbus.DBusException:
|
||||||
|
# We couldn't join GPaste over D-Bus, use native clipboard
|
||||||
|
self.send_password_to_native_clipboard(base_args, name, field)
|
||||||
if "otp" in base_args:
|
if "otp" in base_args:
|
||||||
self.notify("Copied OTP password to clipboard:", body=f"<b>{name}</b>")
|
self.notify("Copied OTP password to clipboard:", body=f"<b>{name}</b>")
|
||||||
elif field is not None:
|
elif field is not None:
|
||||||
@ -156,45 +207,8 @@ class SearchPassService(dbus.service.Object):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.notify("Copied password to clipboard:", body=f"<b>{name}</b>")
|
self.notify("Copied password to clipboard:", body=f"<b>{name}</b>")
|
||||||
except subprocess.CalledProcessError as e:
|
except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError) as e:
|
||||||
self.notify("Failed to copy password!", body=e.output, error=True)
|
self.notify("Failed to copy password or field!", body=str(e), error=True)
|
||||||
except RuntimeError as e:
|
|
||||||
self.notify("Failed to copy field!", body=e.output, error=True)
|
|
||||||
|
|
||||||
def send_password_to_native_clipboard(self, base_args, name, field=None):
|
|
||||||
if field is not None:
|
|
||||||
self.notify(
|
|
||||||
"Cannot copy field values.",
|
|
||||||
body="This feature requires GPaste.",
|
|
||||||
error=True,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
pass_cmd = subprocess.run(base_args + ["-c", name])
|
|
||||||
if pass_cmd.returncode:
|
|
||||||
self.notify("Failed to copy password!", error=True)
|
|
||||||
elif "otp" in base_args:
|
|
||||||
self.notify("Copied OTP password to clipboard:", body=f"<b>{name}</b>")
|
|
||||||
else:
|
|
||||||
self.notify("Copied password to clipboard:", body=f"<b>{name}</b>")
|
|
||||||
|
|
||||||
def send_password_to_clipboard(self, name):
|
|
||||||
field = None
|
|
||||||
if name.startswith("otp "):
|
|
||||||
base_args = ["pass", "otp", "code"]
|
|
||||||
name = name[4:]
|
|
||||||
else:
|
|
||||||
base_args = ["pass", "show"]
|
|
||||||
if name.startswith(":"):
|
|
||||||
field, name = name.split(" ", 1)
|
|
||||||
field = field[1:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.send_password_to_gpaste(base_args, name, field)
|
|
||||||
except dbus.DBusException:
|
|
||||||
# We couldn't join GPaste over D-Bus,
|
|
||||||
# use pass native clipboard copy
|
|
||||||
self.send_password_to_native_clipboard(base_args, name, field)
|
|
||||||
|
|
||||||
def notify(self, message, body="", error=False):
|
def notify(self, message, body="", error=False):
|
||||||
try:
|
try:
|
||||||
|
@ -16,7 +16,7 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-build
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
|
|
||||||
%description
|
%description
|
||||||
A Gnome Shell search provider for zx2c4/pass (passwordstore.org) that sends passwords to clipboard (or GPaste).
|
A Gnome Shell passwords search provider for zx2c4/pass (passwordstore.org) and Bitwarden/Vaultwarden that sends passwords to clipboard (or GPaste).
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%setup -q -n %{name}-%{version}
|
%setup -q -n %{name}-%{version}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user