Skip to content

Commit

Permalink
v0.8.0: migrate to Poetry & add configurable binary
Browse files Browse the repository at this point in the history
fixes: #5
  • Loading branch information
nazarewk committed Oct 20, 2022
1 parent f4e6184 commit 9c9e2cc
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 75 deletions.
8 changes: 4 additions & 4 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ clean:

# see https://realpython.com/pypi-publish-python-package/#build-your-package
build: clean
python -m build
poetry build

# see https://realpython.com/pypi-publish-python-package/#upload-your-package
publish:
python -m twine check dist/*
python -m twine upload dist/*
# poetry config http-basic.pypi __token__ pypi-<api-token>
publish: build
poetry publish
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@

This is a [`pass`](https://www.passwordstore.org/) backend for [`keyring`](https://pypi.org/project/keyring/)

Install with `pip install keyring-pass` and set the following content in your [`keyringrc.cfg`](https://pypi.org/project/keyring/#config-file-path) file:
Install with `pip install keyring-pass` and set the following content in
your [`keyringrc.cfg`](https://pypi.org/project/keyring/#config-file-path) file:

```ini
[backend]
default-keyring=keyring_pass.PasswordStoreBackend
default-keyring = keyring_pass.PasswordStoreBackend
```

You can modify the default `python-keyring` prefix for `pass`, by:

- adding following to `keyringrc.cfg`:

```ini
[pass]
key-prefix=alternative/prefix/path
binary=gopass
```

- (for `keyring` version 23.0.0 or higher) setting environment variable `KEYRING_PROPERTY_PASS_KEY_PREFIX`
- (for `keyring` version 23.0.0 or higher) setting environment variables:
- `KEYRING_PROPERTY_PASS_KEY_PREFIX`
- `KEYRING_PROPERTY_PASS_BINARY`

- You can clear the path (start from root), by setting above to `.` or an empty value (just `key-prefix=` on the line).


## Test your setup

You can check if your setup works end-to-end (creates, reads and deletes a key from password store).
Expand Down
86 changes: 55 additions & 31 deletions keyring_pass/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import codecs
import configparser
import functools
import os
import shutil
import subprocess
Expand All @@ -12,44 +13,57 @@


def command(cmd, **kwargs):
kwargs.setdefault('stderr', sys.stderr)
kwargs.setdefault("stderr", sys.stderr)
try:
output = subprocess.check_output(cmd, **kwargs)
except subprocess.CalledProcessError as exc:
pattern = b'password store is empty'
pattern = b"password store is empty"
if pattern in exc.output:
raise RuntimeError(exc.output)
sys.stderr.write(exc.stdout.decode('utf8'))
sys.stderr.write(exc.stdout.decode("utf8"))
raise
return codecs.decode(output, 'utf8')
return codecs.decode(output, "utf8")


class PasswordStoreBackend(backend.KeyringBackend):
pass_key_prefix = 'python-keyring'

def __init__(self):
super().__init__()
self._load_config()
@functools.cache
def _load_config(
keyring_cfg=os.path.join(platform.config_root(), "keyringrc.cfg"),
):
cfg = {}
if not os.path.exists(keyring_cfg):
return cfg

def _load_config(self):
keyring_cfg = os.path.join(platform.config_root(), 'keyringrc.cfg')
if not os.path.exists(keyring_cfg):
return

config = configparser.RawConfigParser()
config.read(keyring_cfg)
config = configparser.RawConfigParser()
config.read(keyring_cfg)
for attr, option in PasswordStoreBackend.INI_OPTIONS.items():
try:
self.pass_key_prefix = config.get('pass', 'key-prefix')
cfg[attr] = config.get("pass", option)
except (configparser.NoSectionError, configparser.NoOptionError):
pass
return cfg


class PasswordStoreBackend(backend.KeyringBackend):
pass_key_prefix = "python-keyring"
pass_binary = "pass"

INI_OPTIONS = {
"pass_key_prefix": "key-prefix",
"pass_binary": "binary",
}

def __init__(self):
for k, v in _load_config().items():
setattr(self, k, v)
super().__init__()

@properties.classproperty
@classmethod
def priority(cls):
if not shutil.which('pass'):
raise RuntimeError('`pass` executable is missing!')
binary = _load_config().get("pass_binary", cls.pass_binary)
if not shutil.which(binary):
raise RuntimeError(f"`{binary}` executable is missing!")

command(['pass', 'ls'])
command([binary, "ls"])
return 1

def get_key(self, service, username):
Expand All @@ -61,28 +75,38 @@ def get_key(self, service, username):

def set_password(self, servicename, username, password):
password = password.splitlines()[0]
inp = '%s\n' % password
inp = "%s\n" % password
inp *= 2

command(['pass', 'insert', '--force', self.get_key(servicename, username)], input=inp.encode('utf8'))
command(
[
self.pass_binary,
"insert",
"--force",
self.get_key(servicename, username),
],
input=inp.encode("utf8"),
)

def get_password(self, servicename, username):
try:
ret = command(['pass', 'show', self.get_key(servicename, username)])
ret = command(["pass", "show", self.get_key(servicename, username)])
except subprocess.CalledProcessError as exc:
if exc.returncode == 1:
return None
raise
return ret.splitlines()[0]

def delete_password(self, service, username):
command(['pass', 'rm', '--force', self.get_key(service, username)])
command([self.pass_binary, "rm", "--force", self.get_key(service, username)])


if __name__ == "__main__":
svc = "test"
user = "asd"
pwd = "zxc"

if __name__ == '__main__':
svc = 'test'
user = 'asd'
pwd = 'zxc'
keyring.set_keyring(PasswordStoreBackend())

try:
keyring.set_password(svc, user, pwd)
Expand All @@ -92,5 +116,5 @@ def delete_password(self, service, username):
keyring.delete_password(svc, user)

if returned != pwd:
print('{} != {}'.format(returned, pwd))
print("{} != {}".format(returned, pwd))
sys.exit(1)
Loading

0 comments on commit 9c9e2cc

Please sign in to comment.